4.2 用架构思维控制复杂

虽然说认识到软件系统的复杂本质并不足以让我们应对复杂,并寻找到简化系统的解决之道,然而,如果连导致软件复杂的本源都茫然不知,又怎么谈得上控制复杂呢?既然认为导致软件系统变得复杂的成因是难以理解与不可预测,那么就需要学会使用各种手段,尝试与软件系统的规模、结构以及变化作斗争。

4.2.1 分而治之,控制规模

针对规模带来的复杂度,应注意克制“做大做全”的贪婪野心,尽力保证系统的小规模。简单来说,就是分而治之的思想,遵循“小即是美”的设计美学。丹尼斯·里奇(Dennis Ritchie)从大型项目Multics的失败中总结出KISS(Keep It Simple Stupid)原则,基于此原则,他将UNIX设计为由许多小程序组成的整体系统。每个小程序只能完成一个功能,任何复杂的操作都必须分解成一些基本步骤,由这些小程序逐一完成,再组合起来得到最终结果。

表面上看,运行一连串小程序很低效。但是事实证明,由于小程序之间可以像积木一样自由组合,所以非常灵活,能够轻易完成大量意想不到的任务。而且,计算机硬件的升级速度非常快,所以性能也不是一个问题。另一方面,当把大程序分解成单一目的的小程序时,开发也会变得容易。正是遵循了这样的设计哲学,UNIX才能在短短几个月内问世。

4.2.2 保持架构的清晰与一致

保持架构的清晰与一致是降低结构复杂度的不二法门。一个好的系统架构必然是清晰直观易于理解的。如何做到这一点?合理的职责分配,良好的封装与抽象,并在约束的指导下为架构建立一致的风格,这是许多良好系统的设计特征。

Roy Thomas Feilding在其博士论文《架构风格与基于网络的软件架构设计》中通过对REST服务架构的推导充分印证了这种设计方法。

他首先从一个空风格(即没有任何约束)开始,增加了“组件可独立进化”的约束,从而引入Client-Server架构风格;接下来添加“无状态”约束,使得之前的风格发生变化,要求状态应全部保存在客户端。这一约束的增加有效地促进了架构质量属性:可见性、可靠性和可伸缩性。当然,这也在一定程度上降低了性能。此外,它可能还会影响到服务器端的一致性,因为状态保存在客户端,因此服务器就无法保证应用行为的一致性。既然影响到了性能,就需要引入某些机制来改善,于是Roy引入了缓存。但缓存的引入又相应地影响到了可靠性,因为缓存数据有可能失效。考虑到应对Web架构的扩展需要支持架构的通用性,所以引入约束“组件之间要有一个统一的接口”。于是,他引入通用性原则,使得架构得以简化,实现与服务解耦,保证了彼此之间独立的进化性。但这种统一却降低了效率,因为统一的接口要求接口之间传递的信息必须是标准的。要获得统一的接口,就需要有多个架构约束来指导组件的行为。之后,Roy 又通过引入分层架构来保证关注点的分离,使得组件具有更好的独立性与进化性。当然,分层架构的缺点也很明显,就是增加了数据处理的开销和延迟。最后,REST服务架构还引入了“按需执行代码”的约束。

通过约束驱动架构风格的过程如图4.9所示。

图4.9

从上述架构驱动的过程中,我们也看到这种利用约束来选择架构风格的方式并不简单。在架构设计的过程中,要考量的质量因素实在太多,而这些质量因素之间又可能相互冲突与制约,这就需要我们根据优先级做好合理的权衡。

Robert C.Martin 分析了诸多架构设计大师提出的各种系统架构风格与模式,包括Alistair Cockburn提出的Hexagonal Architecture、Jeffrey Palermo提出的Onion Architecture、James Coplien与Trygve Reenskaug提出的DCI架构、Ivar Jacobson提出的BCE设计方法,认为这些方法的共同特征是都遵循了“关注点分离”架构原则,于是提出了“Clean Architecture”的思想[23]

Clean Architecture提出了一个可测试的模型,如图4.10所示,无须依赖于任何基础设施就可以对它进行测试,只需要通过边界对象发送和接收对应的数据结构即可。它们都遵循稳定依赖原则,不对变化或易于变化的事物形成依赖。为了实现这一原则,上述模型让外部易变的部分依赖于更加稳定的部分,如领域模型,而非形成相反的依赖关系。这样还可以使实现变得更易于变化,多变的部分依赖于稳定的部分。好的架构就要能够轻松地改变那些易变的决定。

图4.10

Clean Architecture的特征包括:

●独立于框架。

●易于测试。

●与UI无关。

●与数据库无关。

●与任何外部代理无关。

Clean Architecture的目的在于识别整个架构不同视角以及不同抽象层次上的关注点,并为这些关注点划分不同层次的边界,从而使得整个架构变得更为清晰,减少不必要的耦合。

4.2.3 拥抱变化

变化对软件系统带来的影响可以说是无解的,然而我们却不能因此消极颓废。套用Kent Beck 的话来说,我们必须“拥抱变化”。除了在开发过程中尽可能做到敏捷与快速迭代,来抵消变化带来的影响,我们还应该在架构设计层面分析哪些架构质量属性与变化有关。这些质量属性包括:

●可进化性(Evolvability)。

●可扩展性(Extensibility)。

●可定制性(Customizability)。

要保证系统的可进化性,其本质是定义自治的组件。我们需要从内外边界来思考自治组件的设计,准确地说,就是保证组件满足自治的四要素(如图4.11所示):

●最小完备。

●稳定空间。

●自我履行。

●独立进化。

图4.11

要满足系统的可扩展性,首先要学会识别软件系统中的变化点(热点)。常见的变化点包括业务规则、算法策略、外部服务、硬件支持、命令请求、协议标准、数据格式、业务流程、系统配置、界面表现。处理这些变化点的核心就是“封装”,通过隐藏细节、引入间接等方式来隔离变化、降低耦合。一些常见的架构风格,如基于事件的集成、管道过滤器等的引入,都可以在一定程度上提高系统可扩展性。

可定制性意味着可以提供特别的功能与服务。Fielding在《架构风格与基于网络的软件架构设计》中说:“支持可定制性的风格也可能会提高简单性和可扩展性。”在SaaS风格的系统架构中,我们常常通过引入元数据(Metadata)来支持系统的可定制。插件模式也是满足可定制性的常见做法,它通过提供统一的插件接口,使得用户可以在系统之外按照指定接口编写插件来扩展定制化的功能。

许多框架通过引入“洋葱”模型的中间件架构来满足系统的可定制性,例如 Redux、KOA以及Django。如图4.12所示,是KOA通过应用中间件对客户端发送过来的Request进行处理的过程。

图4.12