在《A Philosophy of Software Design[1]》一书中是这样定义复杂性的:
Complexity is anything related to the structure of a software system that
makes it hard to understand and modify the system.
...
Complexity is more apparent to readers than writers. If you write a piece of
code and it seems simple to you, but other people think it is complex, then it is
complex.
...
Your job as a developer is not just to create code that you can
work with easily, but to create code that others can also work with easily.
所有让一个系统难于理解与变更的,都是复杂性。复杂与否的判断标准,取决于代码的读者,而不是写代码的人。
复杂度的来源主要有三方面。
这些理念上的描述都非常抽象。在实际项目上,更具体的一些原则如:
还有一些启发性的原则:
作为程序员,工作不仅仅是写出能正确、高效执行的代码,而是写出能让他人容易理解与维护的代码。
知识泛指一切能影响系统行为的概念、规则与实体,知识一般以数据的形式存在于系统中,但是也有些知识,是以流程的形式,存在于组织协作的模式中。
将知识中心化,是控制系统整体复杂度的重要方式之一。当知识分散在整个系统的不同地方时,就会出现信息完整性和一致性的问题。对于每一种知识,系统中应当只存在一份真实源(Golden source of Truth)。
当一份中心化的知识,由一个团队管理和维护时,可能会对其它团队的使用造成麻烦。尤其是另一个团队对知识的组织与使用方式,有特殊的需要,却又得不到支持与配合的时候。但是这并不是知识中心化本身造成的问题,而是管理与协作问题。将知识分散化,让每个团队自行维护各自需要的部分,或许可以解一时之急,让团队之间可以独立运作,但是在长期会产生诸多由于复杂度失控、一致性缺失而导致的一系列问题。
当中心化的知识,需要被跨团队和部门使用时,知识持有方有责任构建完善的知识访问与使用体系,以便于知识的应用。 对知识的应用及扩展,则可以分散在不同的系统与模块中。
中心化的知识组织形式本身,并不会造成团队之间的合作困难。只有对知识的理解、使用和沟通渠道不健全时,才会对团队间协作造成困难。
同样的事情,在整个系统中,应当只有一个称谓,一种做法。一致性原则可以应用到很多方面[2],包括:
一致性原则的实施,通常会有额外的成本,比如编码风格一致性的保持,需要一定培训和代码审查及相应工具的前期投入。
将大的问题,分解成小的问题,每一个小的问题的复杂度,都会变得更小。模块分解的好坏,也可以通过复杂度来衡量。一个好的模块化,应该是能让这些模块的复杂度的和,尽量小于整体化设计的复杂度。否则就没有分模块化的必要了。
正交,原本是一个几何概念,表示两个面相互垂直。在软件工程领域,当描述两个系统正交时,表示对其中一个系统的变更,对另一个系统没有实质性的影响。在系统架构设计上,让多个系统或服务之间,建立并保持良好的正交性,是降低系统复杂度,提高服务稳定性和可扩展性的最重要的基本原则之一。在Eric S. Raymond的《UNIX编程艺术》一书中,是这样表述的[3]:
Orthogonality is one of the most important properties that can help make even complex designs compact. In a purely orthogonal design, operations do not have side effects; each action (whether it’s an API call, a macro invocation, or a language operation) changes just one thing without affecting others. There is one and only one way to change each property of whatever system you are controlling.
从正交性的定义本就可以发现,正交的系统间最大的优势在于:对一个系统的变更,不会对其它系统产生影响。这既保证了系统的独立演进,也能减少系统的维护成本。维护系统的正交性,核心优势就是能降低整个系统的复杂度。
抽象化原则是关注点分离的一种应用。当我们期望我们的解决方案,能够与其实现细节分离时,就需要对实现细节进行抽象。本质上,我们要将“实现了什么”和“如何实现的细节”分离[4]。
以写日志为例,写日志这个操作,是“实现了什么”,而通过Logback框架写日志,就属于“实现细节”了。为了能让写日志,写日志的实现方式分离,可以引入一个抽象层来定义所有日志想关的操作。然后所有的日志操作,都使用这个抽象层来做日志输出,同时不去关心具体使用哪一个框架。
基于抽象化的概念所构建的上层业务代码,显然会比基于各种具体情况分别构建的业务代码更加稳定与可靠。
slf4j、Apache Commons Logging和Log4j2 API都属于一个这样的抽象层。
抽象化需要有度,过度的抽象不仅仅会导致资源浪费,也会产生长期的认识负担。比如,对上述三个日志抽象层,建立大统一抽象层即属于过度抽象。
抽象过度与否的判定标准是:这个抽象,是对业务能力或需求的抽象,还是对技术问题和实现方案的抽象? 前者是适度的,而后者则有待商榷。
决定要做什么和实际去执行,是两件不同事。他们所需要的信息不同,关注点也不同。决策关注于确定需要做什么及为什么要做,需要广泛的信息支持;而执行层只关心如何把需要做的事情执行完成,所需要的只是一份具体的执行计划。将决策与执行分离,能简化双方的复杂度。
这里的决策,泛指所有会影响最终执行结果的因素。包括:
服务自治性[5]有两个主要体现。一、独立设计:不依赖外部需求来完成自身的合理设计。二、独立运行:不依赖外部服务的可用性来保证自己的可用性。
尤其是在微服务及SOA架构体系下,由于服务数量众多,如果每个服务不能维护好自身的自治性,那么任何服务的变更,就都会需要与众多的其它服务协同。便会严重影响工作效率和产出的质量。
没有哪个系统是创建出来之后就不会再变的,对变化应当抱有一定的预期,并在合理的范围内做出应对。
每个功能或能力被构建出来的时候,通常是为了解决特定场景的特定问题。但是系统设计的解决方案,应当具备一定的通用性(Generality)。
反面原则,通常不会被宣之於口,但是从结果,并不难反推出每一次变更背后的实际上的指导原则。反面原则,会扭曲系统的原有设计,破坏原本已经建立起来的各种优秀品质。
这里的影响,主要是指对水平相关组件的影响。常常出现于多个团队分别负责同一个系统的不同部分的情况下。做设计的一个隐含的要求就是,设计范围局限在负责的模块或组件内,而极力避免那种需要跨团队协作的设计方案,即使那个设计方案可能更合理(从技术角度)。
然而技术问题从来就不是单纯的技术问题,企业的组织架构本来就应该被纳入设计考量的因素,而且这个原则,也在事实上引导着很多的系统设计思路,这也是康威定律得以成立原因。
在原则的应用,总有个度的问题。举例来讲,外部用户直接使用的API,在需要做非向后兼容变更时,经常会需要提供新的接口,而不直接改动原有接口,以免对所有用户产生影响。但是内部API,尤其是仅仅在一个团队内部使用的API,甚至工具类的改用上,也遵循“不动老代码,只加新代码”的话,就并不合适了。
最少改动原则的优势是,更少的成本和更快的速度。通常会在应对突发状况时及追赶项目进度时采用。当最少改动原则被应用时,最大的问题并不是变更少,而是应有的变更前的深度思考与设计的缺失。
John Ousterhout, A Philosophy of Software Design ↩︎
Eric S. Raymond, The Art of UNIX Programming ↩︎