软件架构整洁之道-读书笔记(2)

第三部分:设计原则

通常来说,要想构建一个好的软件系统,应该从写整洁的代码开始做起。这就是SOLID设计原则要解决的问题。SOLID原则的主要作用就是告诉我们如何将数据和函数组织成为类,以及如何将这些类链接起来成为程序。这里的”类“不限于面向对象编程的类,仅仅代表一种数据和函数的分组。

一般情况下,我们为软件构建”中层结构“的主要目标是:

  • 使软件可容忍被改动;
  • 使软件更容易被理解;
  • 构建在多个软件系统中复用的组件。

SOLID原则紧贴于模块级(中层结构)代码逻辑之上,帮助我们定义软件架构中的组件和模块。

SRP,单一职责原则

基于康威定律,软件系统的最佳结构高度依赖于开发这个系统的组织内部结构,这样每个软件模块都有且仅有一个需要被改变的理由。

OCP,开闭原则

如见软件系统想要更容易被改变, 那么其设计必须允许新增代码来修改系统的行为,而非只能靠修改原来的代码。

LSP,里氏替换原则

最初是Barbara Liskov在1988年提出的一个著名的子类型定义原则。可推广为:如果想用可替换的组件来构建软件系统,那么这些组件必须遵守同一个约定,以便互相替换。

DIP,依赖反转原则

该原则之处:高层策略性代码不应该依赖底层实现底层细节的代码,恰恰相反,那些实现底层细节的代码应该依赖高层代码(接口)。

第7章:SRP:单一职责原则

SRP是重要的原则,其实其他原则的基石。它也是最容易误解的原则,很多程序员顾名思义地认为它的意思是:每个模块都应该只做一件事。

”每个模块应该只做一件事“不算错,我们在编写代码的时候应该遵循该法则:每个函数值完成一个功能;但这只是一个面向底层实现细节的设计原则,并不是SRP的全部。

SRP的准确描述应该是:任何一个软件模块都应该只对某一类行为负责。软件模块可能是一个类,一个源文件,或其他组织形式;逻辑上是指”一组紧密相关的函数和数据结构“,模块定义本身就蕴含了SRP原则。

符合SRP的模块只会有一个导致修改的原因,反过来,特定原因导致的修改也被限定在单个模块内。

第8章:OCP:开闭原则

一个设计良好的系统,应该在不需要修改(或很小的修改)原有代码的前提下,就可以轻易被扩展。这其实也是我们研究架构的根本目的。如果对原始需求的小小延伸,就需要对原有的软件系统进行大范围修改,那么这个系统的架构设计显然是失败的。

OCP的主要目标是让系统易于扩展,同时限制每次被修改所影响的范围。实现方式是通过将系统划分为一系列组件,并将这些组件的依赖关系按层次结构进行组织,使得高阶组件不会因为低阶组件被修改而受到影响。

我认为,OCP原则,就是在SRP的基础上,考虑模块的可替换性、可扩展增加性。

第9章:LSP:里式替换原则

LSP原本是来描述子类型定义的:如果对于每个类型S的对象o1,都存在一个类型为T的对象o2,使得操作T类型的程序P在用o2替换o1时行为保持不变,我们就可以将S称为T的子类型。

后面,LSP逐渐演变成了一种更广泛的、指导接口与实现方式的设计原则。这里的”接口“可以是java风格的interface,也可以像Ruby一样几个类公用一样的方法签名,甚至可以是几个服务响应同一个REST接口。

第10章:ISP,接口隔离原则

接口隔离原则解决这样一种情景:不同的场景下分别需要使用某个模块的不同操作,比如op1~opN,我们应该将这些操作接口隔离成若干组,这样操作接口opN的修改,不会影响到依赖op1的代码。

在一般情况下,任何层次的软件设计如果依赖于不需要的东西,都会是有害的。从源代码层次来讲,这样的依赖会导致不必要的重新编译和重新部署。

对更高层次的软件架构设计来说,问题是类似的:假设软件架构师在设计系统S时,想要引入某个框架F,而F的作者将其捆绑在一个特定的数据库D上,那么就形成了S依赖于F,F又依赖于D的关系。如果D包含了F不需要的功能,那么这些功能对S来说也是不需要的,而D中这些功能的修改会导致S的重新部署,更糟糕的是还有可能导致S运行出错。

所以ISP更广泛的理解是:任何层次的软件设计如果依赖了它并不需要的东西,就会带来意料之外的麻烦。

第11章:DIP:依赖反转原则

DIP告诉我们,如果想要设计一个灵活的系统,在源代码层次的依赖关系中,就应该多引用抽象类型,而非具体实现。也就是说,在java这类静态类型的语言中,应该精良import那些包含接口、抽象类的云未见,应该引用任何具体实现。

显而易见,把这条设计原则当成金科玉律加以严格执行是不现实的,因为软件系统在实际构造中不可避免地需要依赖到具体实现,比如java中的String类型,我们没有必要也不应该将其转化为抽象类型。

因为String类型非常稳定,极少修改,而且修改也受到严格的控制。同理,在运用DIP时,我们也不必考虑稳定的操作系统或者平台设施,因为这些接口很少会有变动。我们主要应该关注的是软件系统内部那些经常会发生变动的具体实现模块,这些模块不停地开发,也就常出现变更。

稳定的抽象层

我们每次修改抽象接口时,一定回去修改对应的具体实现。但反过来,当我们修改具体实现时,却甚少要去修改对应的抽象接口,所以抽象接口比实现更稳定。

优秀的架构师会花费很大精力来设计抽象接口,以减少未来对其进行改动,以下是几条编码原则:

  1. 应在代码中多使用抽象接口,尽量避免使用那些多变的具体实现类;
  2. 不要在具体实现类上创建子类(上一条规则也隐含了这个意思);
  3. 不要覆盖包含具体实现的函数(覆盖它实际上也是在依赖它);
  4. 应该避免在代码中写入与任何具体实现相关的名字,或者其他容易变动的事物的名字。

我们总需要要创建具体实现的对象,为了遵守上述编码原则,我们必须对那些易变对象的创建过程做一些特殊处理,比如运用工厂模式。更普遍地,系统中至少存在一个具体实现组件——main组件,它负责创建具体的实现对象,并初始化它们之间的关系。

第四部分:组件构建原则

大型软件系统的构建过程与建筑物的构建很类似,都是由一个小组件组成的。所以如果说SOLID原则是用于指导我们如何将砖块砌成墙与房间,那么组件构建原则就是指导我们如何将这些房间组合成房子。

第12章:组件

组件是软件的部署单元,是软件系统中可独立完成部署的最小实体。对于java来说,它的组件是jar文件,.Net是DLL文件。总而言之,对于编译运行的语言,组件是一组二进制文件的集合。而在解释运行的语言中,组件则是一组源代码文件的集合。

我们可以将多个组件链接成一个可执行文件,也可以通过插件形式来动态加载。无论何种形式,设计良好的组件都应该永远保持可被独立部署的特性,这同时意味着这些组件应该可以被单独开发。

第13章:组件聚合

那么,究竟哪些类应该被组合成一个组件呢?这是一个非常重要的设计决策,本章给出三个基本原则:

1、REP:复用/发布等同原则

组件复用的最小粒度应该等同于其发布的最小粒度。REP原则初看起来好像不言自明,毕竟如果想要复用某个组件的话,必须要求该组件的的开发由某种发布流程来驱动,并且有明确的发布版本号,以及发布带来的变更说明。

从架构设计的角度看,REP原则就是指组件中的类和模块必须是彼此紧密相关的,也就说,一个组件不能由一组毫无关联的类和模块组成,它们之间应该有一个共同的主题或者大方向。

从另个一视角看,问题就没那么简单了。该原则要求组件汇总包含的类和模块,应该是可以同时发布的(由相同的作者维护,由相同的原因修改,作为整体对用户有意义)。

REP原则很重要,但是很薄弱,因为它没有清晰地定义出到底应该如何将类和模块组合成组件。所以CCP和CRP会从两个相反的角度来对这个原则进行有力的补充。

2、CCP:共同闭包原则

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

这是SRP原则在组件层面的再度阐述。正如SRP原则提到的“一个类不应该同时存在多个变更原因”一样,CCP原则也认为一个组件不应该同时存在着多个变更原因。

对于大部分系统来说,可维护性的重要性要远远高于可复用性。如果程序中的代码必须要进行某些变更,那么这些变更最好都体现在同一个组件中,这样我们只需要重新部署该组件,其他组件则不需要被重新验证、重新部署了。

总而言之,CCP的主要作用就是提示我们:要将所有可能被一起修改的类集中在一处,如果两个类紧密相关,不管是源代码层面还是抽象理念层面,永远会被一起修改,那么它们就该归属于同一个组件。通过遵循该原则,我们可以有效降低软件发布、验证、部署所带来的压力。

另外CCP原则和OCP原则也是紧密相关的,CCP所讨论的就是OCP中的“闭包”。OCP认为一个类应该便于扩展,抗拒修改;100%的闭包是不可能的,我们只能战略性地选择闭包范围。在设计组件时,我们要根据历史经验和预测能力,尽可能地将需要被一同变更的的哪些点聚合在一起。

3、CRP:共同复用原则

类很少被单独复用,更常见的情况是多个类同时作为某个可复用的抽象定义被共同复用。CRP指导我们将这些类放在同一组件中;反之,就应该放入不同的组件。

每当一个组件引用了另一个组件时,就等于增加了一条依赖关系,虽然仅牵涉被引用组件的一个类,但它所带来的组件依赖关系丝毫没有减弱。当被引用组件发生变更时,引用者一般也需要做出相应的变更,及时不是代码级别的变更,一般也免不了重新编译、验证、部署。

所以CRP原则中,关于哪些类不应该放在一起的建议是更为重要的内容。CRP原则实际上是ISP原则的一个普适版,ISP建议我们不要依赖带有不需要的函数的类,而CRP则建议我们不要依赖带有不需要的类的组件。

CRP原则和ISP原则可以用一句话来概括:不要依赖不需要用到的东西。

4、三个原则的关系

上述三个原则之间彼此存在竞争关系,REP是总原则,CCP是粘合性原则,它会让组件变得更大,而CRP原则是排除性原则,让组件尽量变小。架构师的任务就是要在这些原则中间进行取舍。

过于关注CRP会导致,组件粒度过细,即是是简单的变更也会涉及到许多组件。相反,过于关注CCP,组件粒度过大,则导致该组件过于频繁地发布。

优秀的架构师,应当能够依据项目状态来调整侧路,项目早期CCP原则比CRP更重要,因为这一阶段研发速度比复用性更重要。随着项目成熟,其他项目会逐渐开始对产生依赖,复用性目标凸显出来。

第14章:组件依赖关系

1、无环依赖原则

系统中所有组件之间的依赖关系会形成一个有向图,这个图中不应该出现环。假设A,B,C三个组件之间形成了A->B->C->A这样的环状依赖关系,此时组件的修改发布会变得异常困难。试想一下,假设组件C发布了一个新的版本,那么依赖C的B可能要做出变更,接着依赖B的A可能要做出变更,进而又可能影响到了C;问题的本质是:变更的影响顺着组件依赖路径逆向地传递,如果存在环形依赖路径,管理的难度指数上升。

打破循环依赖有两种方法:

  • 应用依赖反转原则(DIP),让组件C不再依赖组件A,反过来让A依赖C;
  • 创建一个新的组件D,让A和C都依赖D。

2、稳定依赖原则

依赖关系必须指向更稳定的方向:一个组件只能依赖比自己更稳定的组件。

关于组件的稳定性度量,有一个参考指标 = 依赖自己的组件数量 / 自己依赖的组件数量。这个指标的原理是,一个组件如果被很多组件所依赖,那么它的修改难度就越大,稳定性就越好(注意:这是结果,不是原因)。

并不是所有组件都应该是稳定,否则系统就无法变更了。架构设计的目的之一,就是要决定哪些组件稳定,哪些组件不稳定。

你可能感兴趣的:(架构设计,java,架构)