最近读了《架构整洁之道》(https://u.jd.com/5nzQJQ) 这本书,这是由Bob大叔所写的一本关于软件架构的著作。书中开篇指明了软件架构的核心目的是用最小的人力成本来构建和维护整个软件系统。那么要如何达到这个目的呢?全书共分为6个部分,就这个问题分别从编程范式、设计原则和组件构建原则等方面进行了论述。
第一、二部分 绪论和编程范式
目前业界所流行的三大编程方式分别是结构化编程,面向对象编程以及函数式编程。结构化编程应该是最早出现的编程范式,它通过对程序控制权的直接转移进行了限制和规范(如goto语句等)。而面向对象编程则在它的基础上引入了多态的概念,来限制用户对函数指针的使用。最后第三大范式是函数式编程,它的核心是通过变量的不变性来实现严格的赋值限制。
其中,结构化编程范式为我们带来了将庞大的软件系统功能通过降级拆分的最佳实践。同时赋予了我们创造可证伪程序单元的能力。从最小的函数到大的组件的开发过程,其实就是一个科学证伪(测试)过程。
众所周知,面向对象编程语言的三大特性是封装,继承和多态。而这三大特性并不是最本质的,毕竟在类似C++这类语言当中,也能够通过编程技巧来实现类似的效果。从软件架构的角度来看,面向对象编程本质上是通过多态的方式来实现代码控制关系的转移。我们利用依赖反转等技术,可以很轻易地实现源代码级别依赖关系的反转,从而更好地将高层组件与底层组件相分离。
函数式编程引入了不可变性这一个核心概念,从而利用该方式可以避免结构化编程和面向对象编程中所遇到的竞争,死锁以及并发更新等问题,所以该范式也在处理多线程、分布式的编程领域都有比较大的应用。另一方面由于变量的不可变,使得对于状态的追溯变得更加容易。这在复杂的日志事务系统中都能找到其应用场景。
第三部分 设计原则
好的软件架构基本上要遵循SOLID的设计原则。那么什么是SOLID原则呢?它其实是Single Responsibility Principle(单一职责原则)、Open Closed Principle(开闭原则)、Liskov Substitution Principle(里氏替换原则)、Interface Segregation Principle(接口隔离原则)和Dependence Inversion Principle(依赖反转原则)这五个原则的首字母联合起来的缩写形式。这些设计原则可以用来指导我们如何将数据和函数组织成类。
单一职责原则是要将服务的不同行为者之间的代码进行切分,从而保证在修改某一个类的时候,不会影响另一个类的行为。为了践行这个原则,最简单的方式是将对象中数据和行为分离,另一种方法就是将共用的逻辑和其对应的函数放在一起,这样对于对象来说在内部它们是共用的,但是对于外部的对象来说却是封闭的。
开闭原则强调的是可扩展性,软件在不需要修改的前提下,就可以轻易的被扩展。那么我们应该如何实现该原则呢?主要方式是可以将系统划分为一个个单独的组件,并且将这些组件分门别类,根据它们的依赖关系,按层级进行划分,同时可以利用依赖反转等编程技巧,使得高阶组件不会因为低阶组件频繁修改而受到影响。
在面向对象编程里面,继承通常是实现里氏替换原则的一个有力工具。为了不违反该原则,我们需要做到的是有着相同父类的两个子类,可以在需要的时候进行相互替换,并且系统还能正常工作。在软件架构的设计中,一旦违背了该原则来建造系统架构,就不得不添加额外的复杂的机制应对了。
接口隔离原则是为了避免软件中对于不需要模块的依赖而导致不必要的重新编译和重新部署。所以我们通常采用的方式是将系统的不同的操作隔离成对应的接口进行解决,这些接口则可以通过组合等方式进行协同工作。
最后一个原则是依赖反转原则,它主要讲的是要在源代码的依赖关系当中多引用抽象类型的具体实现,这样可以保持系统的灵活性。应用该原则主要是针对比较稳定的抽象层来说的。
第四部分 组件构建原则
组件是软件系统当中可以独立部署的最小实体。根据不同的编程语言通常以jar,.dll或者exe等文件形式存在。组件还能以动态加载的插件形式来独立部署。早期系统中,程序的编译和加载都十分缓慢,所以为了缩短软件代码的编译加载时间,程序员们将功能独立的源代码进行单独编译。它们后来就形成了库函数。而随着函数库中函数规模增加,调用函数时,内存地址空间的定位就变得比较复杂。后面又衍生出了可重定位的二进制文件和链接加载器的技术。在经历了大约50年的演变之后,组件化的插件式架构在系统软件架构当中就已经变得习以为常。
了解了组件的概念之后,我们就得知道在软件中是依据哪些原则将相关的代码进行聚合,从而形成一个个组件的。
主要依赖的是以下三大原则:
第1个原则是复用发布等同原则,这个原则其实讲的就是软件复用的最小粒度应该等同于其发布的最小粒度。从程序开发者的角度来说,这就要求我们保证组件当中的模块代码都必须是强相关的,应该有一个共同的主题,从而保证在每一次发布的时候,这些相关的代码都可以被同时发布,还可以共享相同的版本号和发行文档。该原则强调的是组件的可复用性。
第2个原则就是共同闭包原则,本质上和前面所说的单一职责原则是一致的。该原则强调一个组件当中不应该存在多个变更的原因,此时该原则更加强调的是组件的可维护性。
第3个原则是共同复用原则,意思是不要强迫一个组件的用户依赖他们不需要的东西。
这三大原则之间随着业务的不同,考虑的侧重点也不同,需要同时平衡可复用性和可维护性,而且这种平衡关系本身也会随着项目的发展而不断变化。
组件开发完成之后,我们需要进一步思考的是如何处理组件跟组件之间的关系,也就是如何将组件进行耦合。组件和组件之间通过依赖的方式进行组合协作,那么在此时最容易产生的问题就是循环依赖。为了避免循环依赖,我们主要有两种方式,第一种方式是引入接口的方式进行依赖反转。另一种方式是创建一个新的组件,并且让要解耦的两个组件都依赖于它。为了系统的稳定性,我们通常会希望尽可能依赖比较稳定的组件,那么,为了实现这个目标可以将稳定组件至于底层,而那些频繁变更的组件位于顶层。我们知道抽象组件通常是非常稳定的,因此在必要的时候可以创建一些不包含任何可执行代码的抽象组件。