Pulpcode

捕获,搅碎,拼接,吞咽

0%

做加法不做减法

最近在新机器上发布代码的时候发现了一个bug。但是我的老机器从来没有报错,查了以后才发现代码中引用了一个已经被我从git项目中移除的旧包。旧机器不报错是因为在发布的时候远程主机上这个文件并没有被删除,新机器报错是因为git中早已经没这个文件了。

实际上在发布工具有一个配置选项可以选择是否删除发布项中不存在的文件,但是如果你勾选此项,发布工具还是会警告提示你,是否确定勾选此项,因为这样可能会让服务出错。比如就我们那个服务而言,项目所在的文件夹中,还有由另一个服务负责定时更新的一些配置,所以如果你删除了git项目中不存在的文件,那么这些配置项也就都没了。

“软件设计,都是只做加法,不做减法。”,我认为这其实是编程中常见的设计思路,本文就来举例考据一下。要注意的是本文讨论的是编程技术底层实现的思路,与交互功能设计的那一套“做减法”的思想不矛盾也无关。

数据库与orm的同步

这个我在之前的博客提到过,在使用java或者c#框架的orm时,这类工具往往能够通过数据库表来生成业务模型,也能够通过业务模型去直接生成数据库表。
而你在使用orm的时候,到底在使用什么类型的数据库对你而言完全是透明的。你需要做的仅仅是配置一下连接字符串就可以了。

如果我们通过orm生成了表结构,但是在开发过程中,我们根据需要修改了业务类,那么orm和表结构应该如何保持同步?

比如现在有如下的orm

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Student : BaseObject
{
private string _name;
public string Name
{
get { return _name; }
set { SetPropertyValue("Name", ref _name, value); }
}

private int _age;
public int Age
{
get { return _age; }
set { SetPropertyValue("Age", ref _age, value); }
}
}

我们根据这个orm也生成了相应的表结构,之后我们根据需求修改了这个orm,添加了一个字段:

1
2
3
4
5
6
private string _class;
public string Class
{
get { return _class; }
set { SetPropertyValue("Class", ref _age, value); }
}

那么你在启动框架之后,会发现表结构中也增加了这个字段。

但是如果你的需求又进行了修改,不要这个字段了,你在orm中删除了这个字段,启动框架之后,你会发现数据库中的这个字段并没有删除。

如果你在业务模型中将这个Age这个字段重命名为AgeNew,启动框架后,数据库表也不会修改这个字段名,而是新建一个叫AgeNew的字段名。

这种设计思路也就是所谓的“只做加法,不做减法。”防止数据丢失为主,而不是死磕“强一致性。”

有一件有趣的事是如果你将字段class的类型修改为int型。数据库也没有发生任何变化,但是在读取或写入的时候,会报错。

回退和重做

无论你是使用编辑器,或是使用版本控制工具(比如git),都会发现它可以进行回退和重做。

编辑器的回退和重做操作,我在之前的博客有讲。编辑器的撤销和重做

其根本原理就是保存每一次变化的快照(可能是增量快照),撤销和重做都是在这些快照上进行偏移变换。然后刷到编辑器上与人交互。

还有版本管理器的撤销和重做。git被称为是可以回退到任何一个版本。

比如对于这样一个版本树:

1
1 -> 2 -> 3 -> 4 -> 5

现在的最新版本是5,如果我现在想要回退到3,肯定也不是删除掉4,5。而是以快照3为基础,用其覆盖当前目录的代码。将来提交后,可能就是在3的基础上衍生出的版本6了。

以上两个模型都是为了说明,为了能够保证接口服务的撤销(减法)和重做(加法)的可能。程序的底层设计不得不增量的维护一个变化树。而不是也跟着做减法。

缓存读写设计

我接手过一个缓存系统。它有一个写入口,由一个爬虫负责抓取数据并写入。还有另一个服务负责从缓存读取数据。

早期的设计有很大的缺陷。比如第一次爬虫抓到的是A,B,C,D,E这五条数据,写入缓存。第二次抓取的数据可能变为A,B,C,D,E,F(变多),或者A,B,C,D(变少)。 或者A,B,C,G(有缺又有增) 甚至返回空, 如果我们的系统每次都单纯的进行写入覆盖,那么我们的数据就会发生丢失。

正确的做法是对数据进行增量合并。而在读取的时候,再进行限制,比如我设置只能展示ABC,那就只能展示ABC。但缓存中可能增量了一大堆的数据。

需要注意的是,一定要在展示的时候进行限制,而不是在抓取(写入的时候限制)。比如你现在觉得要ABC,所以你在写入的时候就限制只写入ABC。这样突然要ABCD了,那么就算你改了爬虫的策略,也不能马上生效,除非下一次爬虫抓取触发。这就推迟了你一个功能的生效时间,避免了不必要的麻烦。

缓存增减字段

有这样一个业务场景,就是上线的新功能会修改数据格式,但是现在缓存中还有一部分旧数据,如何保证新老交替。

目前我的办法是在原始的数据格式上增加字段(而不是修改现有字段,即使这两个字段功能一样)然后在获取层判断此字段是否存在,来进行新旧交替。(你可以方便的选择是新旧数据都走新逻辑,还是新走新旧走旧)

需要注意的是,这个数据条目的格式,最好是json或者xml。我们之前的系统,使用了\t分割的字符串,通过索引获取字段,通过条目长度判断数据类型。这都给“增量交替”带来了不必要的麻烦。

因为增量数据,要保证获取数据层,可以选择获取,或者不获取新数据,而不是你增量了之后,强制前端也要修改大片的判断逻辑。这也就暗示,前端应该选择型的解析自己只需要的数据,而不是来多少解析多少。遍历的方式会增加耦合度。当然我是指一个对象内的属性不要遍历。如果是容器本身,当然需要遍历。而后端也不因该在增量后,修改了本身的数据格式。