Everything should be made as simple as possible, but not simpler.
-- Albert Einstein
我们一直在谈简单设计,但究竟什么是简单设计?更具体的说,对于同一个问题,设计决策A和B,究竟哪一个更符合简单设计的要求?
对于这类问题,如果没有一个明确的标尺,那么"简单设计"就不免会成为一句无法评判的空洞口号,让程序设计者无从判断和遵守。
对此,Kent Beck给出了清晰的答案:
- 通过所有测试(Passes its tests)
- 尽可能消除重复 (Minimizes duplication)
- 尽可能清晰表达 (Maximizes clarity)
- 更少代码元素 (Has fewer elements)
- 以上四个原则的重要程度依次降低。
这组定义被称做简单设计原则。
初看上去,这组原则平淡无奇,似乎是一组耳熟能详的原则的罗列。但只要细细品味,就会发现其精妙绝伦之处。
通过所有测试
直观的看,这句话貌似在讲测试:一个项目只有具备完善的自动化测试,才算在做简单设计。
但事实上并非如此。这里提到的测试,真正的意思是客户验收。如果你的项目通过了客户的所有验收条件(Acceptance Criteria),那就说明你们已经完成与客户约定的全部需求。至于验收方式是靠人工还是靠自动化测试则无关紧要。
所以,这句话强调的是对外部需求——包括功能性需求和非功能性需求——正确的完成。
尽可能消除重复
重复,意味着低内聚,高耦合。而消除重复的过程,也就意味着是让软件走向高内聚,低耦合,达到良好正交性的过程。识别和消除重复,对于增强软件应对变化能力的重要程度,怎么强调都不为过。关于这一点,我会另文说明,这里就不再赘述。
不过,并不是所有的重复都可以消除:比如,C++一个源文件里对外部公开的类,其每个public方法原型,除了在源文件里定义时,需要声明一次,还需要在头文件里再次声明。这样的重复,是语言机制的要求,无法消除。
因而,这条原则被描述为最小化重复,而不是消除重复。
尽可能清晰表达
清晰性,指的是一个设计容易理解的程度。注意:这不仅仅是对整洁代码(Clean Code)及声明式设计(Declarative Design)的强调。关于这一点,有着非常有趣的部分,我们在随后的部分谈到。
更少代码元素
这一条是点睛之笔,正是因为它的存在,这组原则才被称做简单设计原则,从而区别于其它设计原则。
在这里,常量,变量,函数,类,包 …… 都属于代码元素。代码元素的数量,通常反映了设计的复杂度。因而,这句话强调的是:尽可能降低复杂度,保持简单。
重要程度排序
这一句最容易让人忽视,却恰恰最为重要。如果第四条是点睛之笔,那么第五条就是将之前四条贯穿起来的那条龙。正是这一句,让你知道当以上四条发生冲突时,应该如何取舍。
我们已经知道,第四条,是简单设计的精髓,但是,它在前四条原则却最不重要。
对于第一条,它强调:简单固然好,但你不能为了简单,而不去实现和客户约定好的需求。(当然,如果需求不合理,你应该在前期通过和客户协商拒绝,或修改。但那是关于需求管理这个话题有关的故事,感兴趣者可以去查阅相关文章和书籍)。
而对于第二条,比如,我们现在有两个类:它们之间有一部分重复代码。为了消除掉这个重复,我们将重复代码提取到一个新的类里。于是两个类变为三个类,增加了一个新的代码元素。这当然让设计变得更复杂了。但这种复杂度产生了更重要的价值(让软件更具备正交性,从而让软件更易于修改),所以,不能为了保持简单,而不去消除这个重复。
对于第三条也是如此,比如下面这句代码中有一个magic number:
a = 1000;
为了让这段代码更容易理解,我们将代码修改为:
const int MAX_NUM_OF_CONNECTIONS = 1000;
a = MAX_NUM_OF_CONNECTIONS;
从而增加了一个新的代码元素。因而也稍微增加了设计的复杂度。但由于这个新的代码元素也产生了相对于简单更重要的价值,在简单和表达力之间,我们应该选择后者。
反向价值
而简单设计的价值,也可以从相反的角度来看:如果新增的一条代码元素,不能产生上述三个价值,它就不应该存在。
对于第一条,你不应该去实现一个客户还不需要的需求,因为那会增加系统的复杂度。
对于第二条,你不应该为还没有出现的重复,或者为尚未出现的变化方向,去增加任何额外的复杂度。比如建立一个抽象接口,却只有一个对应的实现。
而第四条对于前两条的约束,就是我们耳熟能祥的YAGNI(You Aren't Gonna Need It)。它强调,我们要着眼当下,不去为自己猜想出的未来可能性去增加系统的复杂度。
总之,当你看到一个代码元素,没有产生之前三条中的任何一条价值,那么它就应该被删除掉。
而这正是简单设计能够简单的原因。
需求最大
抛开第四条,单看前三条,它们之前的重要程度也是依次降低的。比如,你不应该因为怕产生很难消除、或干脆消除不掉的重复而放弃一个对客户有价值的需求。换句话说,哪怕一个需求会导致重复代码,你也要去实现它。
同样的,如果一个需求,会导致你的设计更加晦涩,你却不应该因为它损害了清晰性、可理解性而拒绝它。
对于这两点,绝大多数人都没有太多争议。(也有一派认为,不应该让需求破坏设计的优雅,当两者发生冲突时,选择优雅)。
最具争议话题的解决
我们经常能够听到一种争议:一些人认为,放在一起的长篇累牍的大块流程代码,反而更容易理解。因为消除重复而导致的单一职责,会将一大段代码分布到不同的类或者模块,为了理解它,就不得不在类或者模块间跳来跳去,反而不容理解了。
另外一部分人并不认同这种看法。他们认为,由于模块或类的单一职责性,其每块逻辑都更加简单清晰,然后在另外一个层面再去看它们之间的交互,就可以很快理解整个逻辑。 这比大坨的面条式代码更容易理解。而对于SLAP(Single Level of Abstraction Priciple)的遵守,会更进一步的增加可理解性。
由于可理解性属于更加个人、更加主观的事情,因而究竟哪种方式更容易理解,可能永远也不会有统一的答案。
而简单设计原则通过第二条和第三条之间的排序,给出了清晰的决策依据:由于消除重复,把一大块代码分隔到了不同的地方,即便团队认为这确实损害了可理解性,但由于重复所导致的恶果更加严重,因而优先选择消除重复。
另外,关于简单设计原则,社区内有多个版本,用词不同,但意思大致相同。关键的差别是第二条和第三条的顺序:即消除重复和提升表达力哪个更重要。有一些人认为表达力比消除重复重要,因而把提升表达力放在第二条。但更多人认同的是本文之前的版本。
对于这一点,我个人的观点是,越是主观的东西,就越不具备可验证性或科学性,因而对于工程技术而言,重要程度就越低。
另外,回归到具体项目里,为了避免争议,在不违背前两点原则的情况下,团队可以根据大多数人的审美和认知,决定怎样的设计才更具备可理解性。事实上,在重复已经被消除殆尽的情况下,对于可理解性问题,无论怎样选择,影响都是局部的。
因而,对于这个问题,团队觉得舒服最重要。
结论
简单设计原则,通过对需求、易修改性、可理解性、复杂度,这四个在设计决策中最关键的因素给出了排序,让简单设计不再一个语义模糊的口号,而是对设计决策给出了清晰的guideline。
根据笔者经验,深入理解、并在项目中反复品味和应用它,可以避免掉很多不必要的争议,也会对设计质量产生非常显著的帮助。
网友评论
而简单设计的原则,虽然抽象出了N多小类,理解一段程序可能要去理解系统框架的整条分支才行,但是这一关过去后,给我们带来的好处是对整体有一个清晰的把握,带来的附加值更大,谁好谁坏,简单明了。