从一个反序列化的例子谈谈如何让设计有规律

举报
飞得乐 发表于 2022/11/18 09:43:32 2022/11/18
【摘要】 今天和同事讨论一个反序列化框架的设计,感觉这个例子很适合拿来谈谈如何做设计的问题:这个例子不需要很深的业务背景就能理解,容易传播和讨论;而且这个设计本身非常简单,很快就能看明白。这里隐去所有业务相关的知识,它们对这个问题没什么影响。该框架的需求是这样的:写一套公共的代码,能够支持从文本字符串中解析出业务实例的数据。比如从"[1,2,3]"字符串中解析出一个整型数组。这种需求在业界有很多成熟实...

今天和同事讨论一个反序列化框架的设计,感觉这个例子很适合拿来谈谈如何做设计的问题:这个例子不需要很深的业务背景就能理解,容易传播和讨论;而且这个设计本身非常简单,很快就能看明白。

这里隐去所有业务相关的知识,它们对这个问题没什么影响。该框架的需求是这样的:写一套公共的代码,能够支持从文本字符串中解析出业务实例的数据。比如从"[1,2,3]"字符串中解析出一个整型数组。

这种需求在业界有很多成熟实现,比如JSON等等。不过由于业务的特殊性,我们需要自己定义一下格式。

同事拿出的设计方案是这样的(省略号部分是具体的格式定义):

  • 整数:……
  • 一维整数数组:……
  • 二维整数数组:……
  • 字符串:……
  • 一维字符串数组:……
  • 结构体:……

我看了这个方案后,说这个设计没有定义出支持的类型范围,只是举出了一些个例。

同事说这个就是框架支持的范围,超出这些类型的都不予支持。

我接着说这个确实可以作为一个范围,但是这样定义范围缺乏规律。比如:为什么不支持三维数组?

同事说:看不到支持的必要,为什么要支持三维数组?不太理解为什么缺乏规律。

……

这是个设计中常面对的问题:当我们定义“支持范围”时,其实是在划定边界。类比到二维空间里面,如果沿着尺子方方正正划一个正方形,它是边界;如果拿笔随便在纸上圈几个点,它也可以作为“范围边界”。但是通常,人们认为前者的范围比后者有规律。有规律带来的好处,一是容易维护,二是更方便对齐利益目标。

每一个设计都是有目标的,即做这个设计是为了达成什么。“数组”这个类型可以有很多维,1, 2, 3, 4, ……这些数字本身没有固有属性上的区别。那么,当我们在“1, 2”和“3, ……”之间切一刀,把数字划分成两个集合,我们就得回答这个设计的目标是什么?它带来了什么好处?

所以,“为什么不支持三维数组”和“为什么只支持一维、二维数组”其实是同一个问题,本质都是需要回答边界在这里的目标。而“没有支持的必要”这种回答其实是躲避了目标的澄清。

更进一步讨论,同事解释说是检查了以前的业务实例,发现只有用一维和二维数组的,没有用三维数组的,所以就不支持了。

这个解释会带出两个问题:

  1. 以前的业务实例有几百个,我们是不是一个不漏的全检查过一遍,这值得怀疑。从后来我提问“是否有字符类型”的回应来看,同事说好像是有,但是一时又找不出来,我怀疑“以前的实例没有XXX”这种断言的严密性不高。更何况,为了三维数组这一个边界的设计,去挨个排查所有以前的实例,这么大的工作量真有必要吗?
  2. 以前的业务实例没有三维数组,为何以后就不能有呢?“三维”这个属性到底是有什么特殊性,导致需要在这里建立边界并且增加约束?

再进一步讨论,同事说是三维数组的实例太过于复杂,对于用户不友好,因此不应当支持。如果以后真出现三维数组的实例,我们就使用权利将其驳回,不允许出现。

这个解释又暴露出两个问题:

  1. 为什么三维的就复杂,一维和二维的就不复杂,“三”这个数字到底有什么特殊性,仍然没讲清楚。
  2. 我们当前在做的是反序列化框架的设计,不是在做实例的易用性设计。实例的易用性好不好,不应当拿到反序列化框架的设计中作为输入,更不应当在框架设计的时候主动去限制实例的业务范围。

第二个问题其实体现的是我们常说的“解耦”。解耦不是挂在嘴边的口号,它是个切实的目标:“以后当XXX变化了,YYY不需要修改”。实例的易用性好不好,是进行实例的易用性设计的时候考虑的事;框架支持什么类型的反序列化,是框架设计的时候考虑的事。这两个设计面向不同的目标,他们互相牵制得越少,各自设计时的自由度就越大;自由度越大,设计时就可以利用越多的资源通向成功。如果进行框架设计的时候,把如何限制实例的易用性作为一个目标纳入进来,那么这两个设计就耦合在一起了。以后如果做实例设计的时候发现需要三维数组,修改实例的设计时,框架的设计就得跟着改。

所有上面这些问题,并不会导致设计无法实施,硬要把它作为一个范围限制是可以执行下去的。我只能说做这种缺乏规律的设计会带来更大的风险。

上面谈了很多目标的问题,下面再来谈一谈维护。

为什么缺乏规律的设计不容易维护?在后来的讨论中已经有了呈现:后续讨论中我们发现支持的类型需要增加一个bool类型,于是,表格中要添加bool的定义。但是显然,这个表不能只增加一个bool,而是要增加boolbool一维数组、bool二维数组……至少3种“类型”。

这样的设计在纸面上改动时,影响的是文档维护工作量,如果体现在代码里面,就是增加代码修改工作量。

如果我来做这个设计,我会怎么做呢?

JSON已经给出了很好的答案,JSON标准经过二十多年,几乎没有什么改动,就是因为它的范围定义非常有规律。

类比JSON,我会这样定义反序列化的范围:

对象 := 整数 | 字符串 | 数组 | 结构体

数组 := [不定个数,相同类型的对象]

结构体 := [固定个数,不一定相同类型的对象]

这样如果要添加bool类型,只修改对象的定义,添加一个bool就行了,数组和结构体都不用改。

这个设计也没有“几维数组不支持”这样的问题。如果真要限制几维数组不该用,放到实例设计的时候去约定,框架不管这个事。实例的约束变化时,框架也不用改。

在软件开发实践中,经常会遇到类似这样的设计问题。在TDD的教程中,曾经有这样一个例子:写一个函数,对字符串进行反转。

首先,程序员看到了几个测试例:输入"a",输出"a";输入"Hello",输出"olleH";输入"world",输出"dlrow"。于是他写出了下面的代码:

string Reverse(const string& input)
{
    if (input == "a") { return "a"; }
    else if (input == "Hello") { return "olleH"; }
    else if (input == "world") { return "dlrow"; }
}

至此,所有用例全部通过,一切圆满。这时有人输入"I'm a student",发现输出不对。程序员可以这样辩解:“我排查了以前的实例,没有这种输入。”“有什么必要支持这种输入?”

……在TDD教学重构后,最终的代码类似这样:

string Reverse(const string& input)
{
    string result = input;
    std::reverse(result.begin(), result.end());
    return result;
}

这个版本和前面那个“穷举”的实现的本质区别,就是它对事物做了抽象,使得设计(逻辑)呈现出了规律。做什么样的抽象,呈现出什么样的规律,决定了一个设计的质量。

这段代码的例子似乎过于简单,可能会让人觉得抽象也就这么回事。但事实上,类似的问题在真实的业务代码中可谓屡见不鲜。我之前就见过一个典型:某通信设备软件中,不带保护组的pw+tunnel、不带保护组的pw+带保护组的tunnel,带保护组的pw+不带保护组的tunnel,全部都独立写一套各自的流程代码。这就是缺乏抽象,没有规律,有时也被叫做“烟囱式建模”、“硬编码”。

道理很简单,但实际做起来,需要严密的推敲,合理的归纳,仔细的论证,对业务的深入理解,以及对于简洁、规律的不懈追求的责任心。

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。