软件设计的哲学 A Philosophy of Software Design
软件设计的哲学(在线阅读) 围绕软件复杂性展开,探讨其本质、成因及解决策略,核心目标是通过合理设计最小化软件复杂性,使系统更易于理解和维护。
在任何程序的生命周期中,复杂性都会不可避免地增加。程序越大,工作的人越多,管理复杂性就越困难。有两种解决复杂性的通用方法,这两种方法都将在本书中进行讨论。
- 第一种方法是通过使代码更简单和更明显来消除复杂性。例如,可以通过消除特殊情况或以一致的方式使用标识符来降低复杂性:
- 解决复杂性的第二种方法是封装它,以便程序员可以在系统上工作而不会立即暴露其所有复杂性。这种方法称为模块化设计。这些模块被设计为彼此相对独立(低耦合),以便程序员可以在一个模块上工作而不必了解其他模块的细节。
理解这些方法的最佳方法是与代码审查结合使用。阅读其他人的代码时,请考虑它是否符合此处讨论的概念,以及它与代码的复杂性之间的关系。在别人的代码中比在您的代码中更容易看到设计问题。
复杂性是什么
学习如何设计软件系统以最小化其复杂性。第一步是了解敌人。究竟什么是“复杂性”?您如何判断系统是否过于复杂?是什么导致系统变得复杂?
复杂性的定义
Complexity is anything related to the structure of a software system that makes it hard to understand and modify the system
复杂性是指那些让系统难以理解或修改的与系统相关的任何事物。
可以采用粗略的数学方法表示:系统的总体复杂度(C)由每个部分的复杂度(cp)乘以开发人员在该部分上花费的时间(tp)加权。
如何判断一个系统的复杂性?
一个简单的判断方式:如果一个软件系统难以理解和修改,那就很复杂。如果很容易理解和修改,那就很简单。假如要实施甚至很小的改进都需要大量的工作,那这个系统就很复杂。
注意:阅读代码(读者)的判断更为重要,如果您编写了一段代码,对您来说似乎很简单,但是其他人则认为它很复杂,那么它就是复杂的。
复杂性的症状
下面讲述几种使系统复杂的症状表现:
- 变更放大(Change amplification):代码需要很多不同地方进行代码修改。
例如:下述改一个背景颜色,(a) 需要改四个地方
- 认知负荷(Cognitive load):代码修改需要开发人员花很多时间学习很多知识才能完成。较高的认知负担意味着开发人员必须花更多的时间来学习所需的信息,并且由于错过了重要的东西而导致错误的风险也更大。
例如,假设 C 中的一个函数分配了内存,返回了指向该内存的指针,并假定调用者将释放该内存。这增加了使用该功能的开发人员的认知负担。如果开发人员无法释放内存,则会发生内存泄漏。
注意:系统设计人员有时会假设可以通过代码行来衡量复杂性。但是,这种观点忽略了与认知负荷相关的成本。有时,需要更多代码行的方法实际上更简单,因为它减少了认知负担。所以复杂性并不能从代码行数简单地衡量。
- 未知的未知(Unknown unknowns):开发人员无法“显而易见”地了解需要修改哪些代码、获得哪些信息,才能完成任务。
例如:图 2.1 (c) 说明此问题。网站用中心变量确定横幅背景色,看似易改。但部分网页用较暗背景色强调,且各页面明确指定该颜色。若背景色改变,强调色须改变以匹配。 不幸的是,开发人员可能意识不到,会更改中央 bannerBg 变量而不更新强调色。即便意识到,也不清楚哪些页面用了强调色,可能得搜索网站每个页面。
注意:这种未知的未知是最可怕的,前 2 种只是增加成本,这种唯一的解决方式只有阅读所有的代码,修改代码困难而有风险。
在(a)中,横幅的背景色在每页中都明确指定。在(b)中,共享变量保留背景色,并且每个页面都引用该变量。在(c)中,某些页面会显示其他用于强调的颜色,即横幅背景颜色的暗色;如果背景颜色改变,则强调颜色也必须改变。
复杂性的原因
复杂性主要由依赖性和模糊性引起的。
- 依赖性:当无法孤立地理解和修改给定的一段代码时,便存在依赖关系。依赖关系是软件的基本组成部分,无法完全消除。依赖性导致变化放大和高认知负荷。
- 模糊性:当重要的信息不明显时,就会发生模糊。很多情况会导致系统模糊,如依赖关系存在不明显、文档/注释不足、不一致性等。这个也是引入“未知的未知”的原因。
因此,如果我们找到最小化依赖关系和模糊性的设计技术,那么我们就可以降低软件的复杂性。
复杂度是递增的
复杂性不是由单个灾难性错误引起的,它堆积成许多小块。单个依赖项或模糊性本身不太可能显着影响软件系统的可维护性。 之所以会出现复杂性,是因为随着时间的流逝,成千上万的小依赖性和模糊性逐渐形成。最终,这些小问题太多了,以至于对系统的每次可能更改都会受到其中几个问题的影响。
作为开发人员很容易说服自己,当前引入的复杂性影响不大,但是日积月累,并且每个人员都这么做就会导致系统复杂性迅速累积。
为了减缓复杂性的增长,必须采用“零容忍”理念,长期且坚定地减缓复杂性的增长。
复杂性应对心态
战略编程
成为一名优秀的软件设计师的第一步是要意识到能跑起来的的代码是不够的。这里会提到概念叫“战术编程”和“战略编程”。
- 战术编程(tactical programming):不会花费时间做设计,重点是快速让某个功能/修改生效。
问题:战术编程的问题是它是短视的。如果您是战术编程人员,那么您将尝试尽快完成任务。也许您有一个艰难的期限。因此,为未来做计划不是优先事项。您不会花费太多时间来寻找最佳设计。您只想尽快使某件事起作用。
如果您进行战术编程(如果编码时总是使用战术式思维方式),则每个编程任务都会带来一些此类复杂性。为了快速完成当前任务,他们每个人似乎都是一个合理的折衷方案。但是,复杂性迅速累积,尤其是如果每个人都在战术上进行编程的时候。 - 战略编程(strategic programming):必须花费时间来改进系统的设计,而不是采取最快的方式来完成当前的项目。
优势:战略性编程需要一种投资心态,这些投资会在短期内让您放慢脚步,但从长远来看会加快您的速度。
但是,无论您预先投入多少,设计决策中都不可避免地会出现错误。随着时间的流逝,这些错误将变得显而易见。发现设计问题时,不要只是忽略它或对其进行修补。花一些额外的时间来修复它。
往往大多数开发人员会采用“战术编程”的方式,但是这种方式在不断地增加复杂性,这些复杂性将来会引起更严重的问题,可能需要花更多时间去处理。
长期心态
但是,用“战略编程”的方式必然引入开发成本,我们也需要考虑以什么方式更好地应对:这里的答案就是用长期心态去应对它。
那么应该投资多少?
大量的前期投资(例如尝试设计整个系统)将不会有效。最好的方法是连续进行大量小额投资 。我建议您将总开发时间的 10%到 20%用于投资。 该金额足够小,不会对您的日程安排产生重大影响,但又足够大,可以随着时间的推移产生重大收益。
如上图,一开始,战术性的编程方法将比战略性方法更快地取得进展。但是随着时间的流逝,战略编程会带来更大的进步。而且长期的“战术编程”可能会导致恶性循环,代码库的糟糕导致优秀工程师的流失,优秀工程师的流失导致代码库更加糟糕。
所以,我们需要始终如一使用长期心态(“战略心态”)应对复杂性,每个工程师在每次修改代码的时候都需要进行对于系统设计的投入(连续的少量投入),这样我们才能获得一个复杂性较低的系统。
降低复杂性的方法
上述主要讲述复杂性的定义和应对心态,下面讲述应对复杂性的一些原则和方法。
设计“深模块”
管理软件复杂性最重要的技术之一就是设计系统,以便开发人员在任何给定时间只需要面对整体复杂性的一小部分。这种方法称为模块化设计。 模块可以采用多种形式,例如函数,类,子系统或服务。
理想情况下,开发人员可以在任何模块中工作,而无需了解其他模块。但是这种理想无法实现,模块必须通过调用彼此的函数或方法来协同工作。 结果模块之间就存在依赖关系,模块化设计的目标是最大程度地减少模块之间的依赖性。
为了管理依赖关系,我们将每个模块分为两个部分:接口和实现。通过将模块的接口与其实现分开,我们可以将实现的复杂性从系统的其余部分中隐藏出来。模块的用户只需要了解其接口提供的抽象。
深模块
“深模块(Deep Module)”:最好的模块设计是那些提供强大功能但具有简单接口的模块。
如图,左边用简单的接口提供了复杂的功能,就代表这个模块很深。
Unix 提供的文件 I/O 机制就是一个例子,文件系统是尤其复杂的,这么多年,系统内核发生了很多变化,但是这 5 个内核调用都没有发生变化。 Unix I/O 接口的现代实现需要成千上万行代码,解决复杂问题,但是对于调用程序员来说,它们是不可见的。
int open(const char* path, int flags, mode_t permissions);
ssize_t read(int fd, void* buffer, size_t count);
ssize_t write(int fd, const void* buffer, size_t count);
off_t lseek(int fd, off_t offset, int referencePosition);
int close(int fd);
浅模块:浅层模块是其接口与其提供的功能相比接近的模块。浅类有时是不可避免的,但是它们在管理复杂性方面没有提供太多帮助。
例如,实现链表的类很浅。操作链表不需要太多代码(插入或删除元素仅需几行),因此链表抽象不会隐藏很多细节。
private void addNullValueForAttribute(String attribute) {
data.put(attribute, null);
}
其实这个接口封装增加了系统复杂性,没有提供任何抽象(考虑接口不比考虑完整实现简单、要记录方法则文档更长、还需要学习)。
下面讲述几种设计“深模块”的关键点:
规避“信息泄漏”
这里会提到两个概念,“信息隐藏”和“信息泄漏”。
“信息隐藏”和深层模块密切相关,如果模块隐藏了很多信息,则可以使系统更容易演化,同时还会减少其接口。这使模块更深。如上述 unix IO 接口通过抽象屏蔽了很多复杂的操作系统信息。
它的反面是“信息泄漏”,当一个设计决策反映在多个模块中时,就会发生信息泄漏。有两种体现方式:
- 接口泄漏:如修改一个功能需要影响多个模块,你把相关信息知识暴露在接口调用中,这里就叫“Interface leakage”。例如模块的接口(如类的公共方法、函数参数、返回值等)直接反映了其内部的设计决策或实现细节,本应隐藏的信息(如内部数据结构、算法逻辑、依赖的格式等)通过接口“扩散”到了其他模块。
- 后门泄漏:即使信息未出现在模块接口中,若多个模块通过“私下共识”共享了某一设计决策(如文件格式、协议细节、数据编码规则等),则发生了后门泄漏。如有一个类写文件,一个类读文件,他们依赖相同的文件格式,如果文件格式修改,两个类都需要修改,这就是“Back-door leakage”。这种更严重,会引起“未知的未知”。
这里会提到几种原则:
-
避免“时间分解”设计风格。在时间分解中,系统的结构对应于操作将发生的时间顺序。在设计模块时,应专注于执行每个任务所需的知识,而不是任务发生的顺序。
如,有一个应用程序,以特定格式读取文件,修改文件内容,然后再次将文件写出。按时间分解,很容易设计三个类,这将导致信息泄漏(文件格式多次出现),但是理论上读写文件的机制应该在同一个模块
-
组合相关功能的代码。组合功能可以简化接口,减少“接口泄漏”。
类并不是越多越好,通常可以通过使类稍大一些来改善信息隐藏。
-
使常见情况尽可能简单。只要有可能,模块就应该“做正确的事”,而无需明确要求。默认值就是一个例子。如之前提到的例子有一个接口分配了内存,理论上它可以默认自动释放内存,调用者不用感知内存释放的存在。
多设计通用模块
设计模块时最普遍的决定就是:是以通用还是专用方式实现它?
In my experience, the sweet spot is to implement new modules in a somewhat general-purpose fashion. The phrase “somewhat general-purpose” means that the module’s functionality should reflect your current needs, but its interface should not. Instead, the interface should be general enough to support multiple uses.
以我的经验,最有效的办法是以某种通用的方式实现新模块。短语“somewhat general-purpose(有点通用)”表示该模块的功能应反映您当前的需求,但其接口则不应该反映您当前的需求。相反,该接口应该足够通用以支持多种用途。
通用模块通常相比专用模块接口更简单,更深入。一般可以问自己几个问题:
- 满足我当前所有需求的最简单的接口是什么?
需要尽可能设计通用、简单的接口,减少接口数量。
注意:参数数量也是复杂度,如果您必须引入许多其他参数以减少方法数量,那么您可能并没有真正简化事情。
- 在多少情况下会使用此方法?
假如专门为特殊用途设计的,需要再思考下它有没有必要。因为它会增加系统复杂性。
- 这个 API 对我当前的需求来说容易使用吗?
如果您必须编写许多其他代码才能将类用于当前用途,那么这是一个危险信号,即该接口未提供正确的功能。
使模块具有某种通用性是降低整体系统复杂性的最佳方法之一。
减少配置参数
如何创建更深层类还有另一种思考方式——把复杂度隐藏在类的内部。
假设你在开发一个新模块,并发现一个不可避免的复杂性,哪个更好:应该让模块的使用者处理复杂性,还是应该在模块内部处理复杂性? 如果复杂度与模块提供的功能有关,则第二个答案通常是正确的答案。 大多数模块拥有的使用者多于开发人员,因此麻烦开发人员比麻烦使用者更好。你应该使模块的使用者尽可能轻松。
作为开发人员,很容易以相反的方式行事:解决简单的问题,然后将困难的问题推给其他人。这样的方法短期内会使您的生活更轻松,但它们会加剧复杂性,许多人必须处理一个问题,而不仅仅是一个人。
例如,配置参数是提高复杂度而不是降低复杂度的一个示例。类可以在内部输出一些控制其行为的参数而不是在内部确定特定行为。
拥护者认为配置参数不错,因为它们允许用户根据他们的特定要求和工作负载来调整系统。在某些情况下,底层基础代码很难知道应用的最佳策略,而用户更加熟悉。这种情况,参数配置可以带来更好的效果。 但是,配置参数还提供了一个轻松的借口,可以避免处理重要问题并将其传递给其他人。在许多情况下,用户或管理员很难或无法确定参数的正确值。在其他情况下,可以通过在系统实现中进行一些额外的工作来自动确定正确的值。 这种情况,应尽可能避免使用配置参数。
总之,导出配置之前问自己:用户(或更高级别的模块)是否能够确定比我们在此确定的更好的值?
当您创建配置参数时,请查看是否可以自动计算合理的默认值,因此用户仅需在特殊情况下提供值即可。理想情况下,每个模块都应完全解决问题。配置参数导致解决方案不完整,从而增加了系统复杂性。
但是也不要做过头,一种极端的方法是将整个应用程序的所有功能归为一个类,这显然没有意义。 如果(a)被降低的复杂度与该类的现有功能密切相关,(b)降低复杂度将导致应用程序中其他地方的许多简化,则降低复杂度最有意义。简化了类的接口。请记住,目标是最大程度地降低整体系统复杂性。
设计有不同抽象的层
软件系统由层组成,其中较高的层使用较低层提供的功能。在设计良好的系统中,每一层都提供与其上、下两层不同的抽象。
假如系统中存在两个有相似抽象的层,那意味着类/模块的拆解有问题。接下里讨论如何发生这种情况,导致的问题以及如何重构以消除问题。
- 透传方法:相似抽象的问题,通常以透传的形式表现出来。透传是一种除了调用另一个方法(其签名与调用方法的签名相似或相同)之外,很少功能的方法。
存在问题:
- 透传方法使类变浅:它们增加了类的接口复杂性,从而增加了复杂性。
- 透传方法表明类之间的责任划分存在混淆。
解决方案:解决方案是重构类,以使每个类都有各自不同且连贯的职责。
- (a): C1 是一个不好的设计,它做了大量的透传方法。
- (b):将较低级别的类直接暴露给较高级别的类的调用者,而从较高级别的类中删除对该功能的所有责任。
- (c):或在类之间重新分配功能。
- (d):如果无法解开这些类,最好的解决方案可能合并它们。
但是有相同签名的方法是不好的吗?并不是的,重要的是,每种新方法都应贡献重要的功能。比如调度器,相同签名调度到不同实现。 所以判断原则就是每种新方法都应贡献必要的功能,直接透传方法是糟糕的。
- 传递变量:跨层 API 重复还有一种表现,就是传递变量。该变量是通过一长串方法向下传递的变量。
存在问题:传递变量增加了复杂性,因为它们强制所有中间方法知道它们的存在,即使这些方法对变量没有用处。
解决方案:
如上图
- (a):cert 在中间并用不上,增加了系统的复杂性。
- (b):查看最顶层和最底层方法之间是否已共享对象,这样的对象本身可以传递参数
- (c):将信息存储在全局变量中,但是全局变量几乎总是会产生其他问题
- (d):最常使用的解决方案是引入一个上下文对象(虽然它也会带来一些问题,比如带来了不明显的依赖关系,导致线程安全等,但是假如是个上下文是个不变量就能规避较多问题)。
更与其说本节叫“设计有不同抽象的层”也可以叫做“引入有意义的代码。 我们需要知道,添加到系统中的每一个设计基础设施,如接口、参数、函数、类或定义,都会增加复杂性,因为开发人员必须了解这个元素。所以不管是抽象层还是变量,都需要考虑到到你需要有收益,你才加入,否则无法对抗复杂性的增益。
做好代码拆分和合并
软件设计有一个基础的问题:给定两个功能,代码应该写在一起还是分开写?
这里提供了基础的判断原则:
- 以降低系统的复杂性(改善模块化)为目标。
- 看来实现此目标的最佳方法是将系统划分为大量的小组件。但是细分的行为会带来额外的复杂性:组件越多就越难以追踪、可能会导致附加代码来管理组件、产生分离使开发人员很难知道这些组件存在、导致重复
- 如果它们紧密相关,则将代码段组合在一起是最有益的。如果各部分无关,则最好分开。代码相关:他们共享信息、它们一起使用、它们在概念上重叠、不看其中一段代码就很难理解。
- 如果信息共享,可以放到一起。比如特定文件读取和写入,或者必须了解到另一个模块的实现才能了解当前模块的代码的实现,他们一般是需要放在一起。
- 如果可以简化接口,可以放到一起。比如本来要调用两个类,现在一个类就做到了。这个我们上面有提到,这将使模块更深。
- 如果可以消除重复,可以放到一起。如果发现反复重复的代码模式,请查看是否可以重新组织代码以消除重复。相同的代码一遍遍出现,说明没有找到正确的抽象。
何时细分的问题不仅适用于类,而且还适用于方法,这里涉及一定要拆分还有一个观点是:方法行数不能超过 X 行。但是,其实长方法不一定是坏的。我们可以看下面的例子。
如图,总体而言,分割一个方法只有在产生更清晰的抽象时才有意义。
- (a)是聚合在一起的方法
- (b)✅拆分了子方法,如果子方法是独立的子任务,并且父方法和子方法完全信息分离(不需要互相了解),拆分是好的,假如做了拆分后,发现经常需要跳转过来看子方法的实现,那就不是好的拆分。
- (c)✅拆分为 2 个方法,理想情况只需要调用其中一个方法即可。如果调用者必须同时调用这两个接口,那这个拆分是不好的。
- (d)拆分了多个方法和层级,会导致接口复杂度大大增加,这个是应该避免的。
结论:拆分或合并的决定应基于复杂性。选择一种结构,它可以最好的隐藏信息,产生最少的依赖关系和最深的模块。
通过定义规避错误
软件中对异常的处理,往往会增加软件的复杂性,需要编写很多代码处理异常,比如异常上报、中断、重试等。
在编写模块接口的时候,很容易想到的就是,我把错误抛出去,让调用者处理就行了。但是其实这种想法,是把复杂性转移了,假如开发接口的人都处理不了的话,调用者基本也很难处理。
所以最好的方式就是设计好接口,让它没有异常可以处理,也就是第一种方式,通过定义规避错误。
这里有一个很典型的例子:
对于文件系统,Windows 操作系统不允许删除文件(如果已在进程中打开文件),为了删除正在使用的文件,用户必须在系统中搜索以找到已打开文件的进程,然后终止该进程,这个很令开发者头疼。但是 Unix 操作系统这块就做得很好,假如当前有进程在使用时,它并不会抛出异常,而是做了标记,等到所有访问进程都关闭了文件,便释放其数据。
第二种处理异常的方式就是屏蔽异常。如,TCP 在其实现中通过重新发送丢失的数据包来掩盖数据包的丢失,因此所有数据最终都将送达,并且客户端不会察觉到丢失的数据包。
第三种方式是做好异常聚合。
如上图,右侧的代码就是做好了异常的聚合处理。不过这里比较重要的需要做好异常分级,需要判断好哪些异常可以一起处理不会影响到系统的恢复。
第四种方式是使应用程序崩溃。这种是最粗暴的方式,通常出现在很少发生,并且错误很难处理的情况。常见的情况比如“内存不足”等,处理这些问题复杂性相当高,收益较低,这个可以根据实际情况去做取舍。
总而言之,我们需要通过设计减少异常,假如必须要抛出异常,需要抛出有意义的异常,这样整体才能降低系统复杂性。
多次设计
我们需要知道,第一次设计系统比较难产出最佳的设计。所以假如想要获得更好的结果,最好多思考设计两遍。
怎么进行多次设计呢?
每次做系统设计的时候,尝试选择不同的方案,并列出它们的优缺点(不管第二个方案有多糟糕,因为了解它的弱点将对你的方案很有启发)。然后可以问自己几个问题(当然可以更多):
- 还有更简单的方式吗?(从复杂性角度思考)
- 这个实现足够通用吗?
- 这个实现更有效率吗?
这不仅能改善设计(降低系统复杂性),还能提高代码设计的能力。
写好注释
代码注释/文档是非常重要的。没有注释的话,无法隐藏复杂性。
误区
大家一般会以几个理由不写注释:
- 我的代码写得很好,不用注释了:注释是抽象的基础。如果调用人员必须阅读代码才能调用接口,意味着这个接口无抽象可言。
- 我没有时间写注释:同样的上面章节我们提到“长期心态”。好的注释对软件的可维护性有很大的影响。其实占用时间很少。
- 注释会过时,产生误导:这个取决于注释的写法(关键的思想是避免重复的文档,并保持文档和代码保持相近)
注释没有意义:这个确实存在,关键点在于我们需要写有意义的注释。
文档和注释能够解决复杂性的问题(包含上述提到的“认知负荷”和“未知的未知”),理清依赖关系和消除模糊性。
怎么做
下面,主要讲述怎么写好注释。
- 应该描述代码不明显的内容:很容易造成的误区是,表述了一段代码的含义,这个是完全没有意义的。注释内容包含两种:
- Low-level comment:讲述变量的单位 / 边界条件等(这将帮助调用者不用阅读代码才能了解内部信息)
- Higher-level comment:帮助调用者理解了代码的整体意图和结构。
- 将注释 / 文档放在代码附近:注释离其关联的代码越远,正确更新的可能性就越小。
- 注释优先于代码:这样才会产生更好的注释,你在设计过程中不断改进注释。一旦延迟,随着代码变多,这些工作的吸引力就会越来越少。
- 避免重复注释 / 文档:重复的注释或者重复的文档往往会导致它们一般是陈旧的。
保持系统的一致性
系统的一致性也是降低复杂性重要的工具。将帮助开发人员更快地以更少的错误,使用相似的工作方式完成任务。
一致性体现有以下几个例子:
- 命名:命名的一致将帮助系统开发人员了解相关的概念。(领域驱动设计中就有这样的理念)
- 编程样式:也就是代码风格指南
- 接口:接口一致,实现不一致
- 设计模式:一致的设计模式将更容易被接受
如何确保一致性呢?
- 约定:就是规范文档
- 执行落地:有规范并不够,需要看落地的情况
- 配套工具,比如 Lint 工具等保障代码检查
- 做好代码审查,代码审阅者越挑剔,团队中的每个人都将更快地学习约定,并且代码越干净。
- 尽量不要更改现有约定 When in Rome, do as the Romans do. 最好参考示例的行为,假如你需要引入不一致的行为,思考几个问题:
- 旧方法是否无法满足?
- 新的方法是否好得多,值得花时间更新旧方法?
- 组织是否确定接受?
总之,保持一致性也是非常重要的,需要在日常的规范、自动检查工具、代码审查中帮助大家达成共识,进而能够降低系统复杂性,以更少的错误来更快地工作。
总结
上述就是本次分享的整体内容,一切都围绕着“复杂性”展开。成为一个优秀的软件工程师是需要在软件设计上面花费(投资)更多的时间,改善系统的复杂性,否则将在花大量时间在复杂的系统中寻找错误。
我觉得假如其他信息都忘了的话,可以记住以下几点:
- 复杂性是递增的,添加到系统中的每一个设施都会增加复杂性,所以都需要考虑对于系统复杂性的影响。
- 需要始终如一地投入降低复杂性,将很快获得回报。
- 软件的设计应该是易于阅读而不是易于编写。