架构整洁之道解读

架构的定义

软件架构,是在交付基本功能的基础上,能够使得系统易于开发、部署、运行和维护,用于支撑软件系统的生命周期。在架构设计中要尽可能长时间地保留尽可能多的可选项。

软件架构师必须是程序员,且是一线程序员,并且是能力最强的一线程序员。当然,代码量未必是最多的。

为什么要提编程范式

  • 结构化编程是对程序控制权的直接转移的限制(限制了goto);
  • 面向对象编程是对程序控制权的间接转移的限制(限制了函数指针);
  • 函数式编程是对程序中赋值操作的限制(限制了变量多次被赋值,避免临界区的竞态问题)。

相比于软件产生之初,每种范式都是为了约束某种编写代码的方式,没有一种范式是在增加新能力。

编程范式和架构有关系吗?当然有,譬如,面向对象的多态是跨越架构边界的手段,函数式编程是规范和限制数据存放位置与访问权限的手段,结构化编程则是各模块算法实现的基础。这和软件架构的三大关注点不谋而合:功能性、组件独立性以及数据管理。

也就是说,编程范式是一种架构约束。

SOLID扩展到架构维度同样适用

SRP:每个软件模块都有且只有一个被改变的理由;
OCP:软件设计必须允许新增代码来改变系统的行为,而非只能靠修改原来的代码;
LSP:如果想用可替换的组件来构建软件系统,那么这些组件就必须遵守同一个约定,以便让这些组件可以相互替换;
ISP:这项设计原则主要告诫软件设计师应该在设计中避免不必要的依赖;
DIP:该设计原则指出高层策略性代码不应该依赖实现底层细节的代码,相反,那些实现底层细节的代码应该依赖高层策略性代码。DIP将会是这本书的核心思想。

组件

组件聚合

组件是软件的部署单元,是整个软件系统在部署过程中可以独立完成部署的最小实体。例如java的jar,C的so等,或者python的一组源文件。动态链接技术,使得组件化的插件式架构已经成为常见的软件构建形式了。

那么,哪些内容应该归到同一组件内呢?要从几个原则说起:

REP:复用/发布等价原则

软件复用的最小粒度应等同于其发布的最小粒度。即划归到同一组件中的类与模块应该是可以同时发布的。这意味着它们共享相同的版本号和版本跟踪,并且包含在相同的发行文档中,这些都应该同时对该组件的作者和用户有意义。

CCP:共同闭包原则

我们应该并且只应该将那些会同时修改,并且为了相同的目的而修改的类放到同一个组件中。

这其实是在组件层面的SRP原则。正如SRP原则提到的一个类不应该同时存在着多个变更原因。CCP原则也认为一个组件不应该同时存在多个变化原因。

CRP:共同复用原则

不要强迫一个组件的用户依赖他们不需要的东西。跟ISP原则类似。反过来讲,当我们决定要依赖某个组件时,最好实际需要依赖该组件中的每个类。

权衡和取舍

实际上,上述三个原则有矛盾的地方。REP和CCP是黏合性原则,可能让组件变的更大,CRP原则是排除性原则,可能让组件变的更小。架构师要清楚这三个原则可能不能同时兼顾,需要在其中进行权衡取舍。

组件张力图

不同的项目特点和项目阶段,所关心的重点也有所不同。一般在项目初期,可能会牺牲一些复用性,而项目后期,随着其它项目对自己的依赖不断增加,则需要更多关注REP原则。

因此,组件的划分原则可能是一个需要不断变化的过程,这需要经验丰富的架构师根据项目的特定阶段的主要矛盾进行实时调整。这也要求软件架构设计要具备这种随时可调整的灵活性。

组件耦合

ADP: 无依赖环原则

最理想的情况是组件间没有依赖,但这是不可能做到的,即便可以做到,这样的组件也是无意义的。反过来,我们要承认依赖存在的必要性,我们需要依赖,但组件依赖关系中不应该出现环。

假如出现循环依赖,则任何一个组件的改动,必然导致该依赖环中的所有组件跟着变化。这与我们期望的最少依赖不相符。

循环依赖

上图中,A的变化,必然导致B和C的重新编译,同理,其中任何一个组件的变化,必然导致另外两个组件的重新编译,甚至重新测试。

解决循环依赖的两种方法:

1.应用DIP原则:

形成依赖倒置

2.新增一个组件(可以认为是适配器组件)

新增适配器组件

通过解除循环依赖,B的变化,不会再引发其它几个组件的变化。

上述两种方式,解决了A和B之间的依赖问题。同理,这种方式可以应用于A和C,B和C之间。但是随着依赖关系的解除,组件的个数也在增加,组件的管理成本也会随之增加,这个需要架构师进行权衡。

SDP:稳定依赖原则

依赖是不可避免的,但是依赖关系必须要指向更稳定的方向。

这个原则太明显了,不展开再阐述。想说明一点,如果组件之间不满足这些原则,可以通过一些手段改造以使得它满足这个原则,没错,它就是DIP。

SAP: 稳定抽象原则

一个组件的抽象化程度应该与其稳定性保持一致。

我们期望被依赖的组件具有稳定的特质,而稳定的组件一般会被很多组件依赖。这样依赖,稳定的组件不应该被经常修改,这就限制了组件的灵活性。怎么调和这个矛盾?没错,就是抽象。稳定的组件应该是抽象的,这样通过OCP原则就可以既满足已有内容的不变性,又满足新增修改的可扩展性。

小结:

组件关系无法在项目之初就确定下来,它是一个随着项目的进行不断扩张和演进的过程,这印证了自上而下设计的不靠谱。另,组件依赖结构图并不是用来描述应用程序功能单元的,它更像是应用程序在构建性维护性方面的一张地图。

软件架构图

自从敏捷之后,代码即设计的思潮让人们完全摒弃了设计图,这其实是一种走极端。仔细观察敏捷大师们,它们不是没有架构图,而是不再纠结于细节。同时架构图的形式也不再严格拘泥于UML形式。

因此:

  • 架构图要有;
  • 要有框,还要有线,表明关系;
  • 框里的描述必须是有业务含义的。

架构设计的核心技术:依赖倒置和策略模式

所有跨越架构边界的处理,都可以考虑依赖倒置和策略模式。


依赖倒置

这里的依赖倒置,为了达到倒置的目的,可以使用更高级的实现手法,这篇文章中进行的精辟的解读。我们在设计组件时一定要关心边界和接口定义的归属。它代表着依赖反转原则在更大的架构层面上的运用。

更大层面的依赖倒置

这个图就是整洁架构右下角那个图

首先要识别出我们自己的核心资产(核心域),那么这个核心域就是所有其它部分需要朝拜的,即所有其它部分需要依赖它,而不是反过来。

这里所谓的核心域是相对的,UI是对于使用者是非核心域,而对于UI开发团队,则是核心域。另外,不能以代码量的大小为划分依据,也不能以涵盖的功能多少为划分依据,唯一的划分依据应该是你维护的代码(或者为你赚钱的那部分代码),那才是你应该保护的核心。

整洁架构同心圆

上图中的同心圆,分别代表了系统软件的不同层次,通常越靠近中心,其所在的软件层次就越高,基本上,外层圆代表的是机制,内层圆代表的是策略。

上述同心圆很像MVC模式,其实MVC的核心思想也是隔离稳定的核心业务和多变的外部表现。我们可能低估了MVC的威力。

一条贯穿整个架构设计的规则:源码中的依赖关系必须指向同心圆的内层,即由底层机制指向高层策略。

具体到代码实现上,即内层应该定义接口让外层去实现(往往借助接口适配器那层来实现),我们不应该让外层发生的任何变化影响到内层。

架构设计的核心目标:

一个良好的架构设计应该围绕着用例来开展,这样的架构设计可以在脱离框架、工具以及使用环境的情况下完整地描述用例。架构师应该花费精力来确保架构的设计在满足用例的情况下,尽可能地允许用户能自由地选择框架、工具等等(类似于在满足建筑框架的基础上,允许用户自由选择建筑材料,例如石材、钢材或木材)。

而且,良好的架构设计应该尽可能地允许用户推迟和延后决定采用什么框架、数据库、UI以及其它工具。良好的架构设计还应该很容易改变这些决定。因此良好的架构要具备足够的灵活性,要警惕把软件(software)做成了固件(firmware)。

因此,良好的架构设计应该只关注用例(核心业务逻辑),并能将它们与其它周边因素隔离。框架和开源第三方软件等免费软件是细节,要跟它们保持好距离,当心被这些东西绑架。

关于测试

每一步架构设计,都要同时问问自己,它便于测试吗?何时对它进行测试?

参考:

【1】《架构整洁之道》--Robert C.Martin著,孙宇聪 译
【2】 lambeta系列博客

你可能感兴趣的:(架构整洁之道解读)