《架构整洁知道》读书笔记
《架构整洁知道》读书笔记
旧书重读,又有了新的体验和感受。多年前读过一次,囿于经历有限,工作中没有使用组件化开发,组件这部分内容对于我来说属于困难区,对该部分的内容只是浅尝辄止,没有深入研究落地过。随着工作经历和经验的增加,自己接触这方面越来越多,组件这部分内容变成了我的拉伸区,再次读这部分内容的时候又有了新的收货,在工作中我也能以此为指导原则对组件做更好的落地。
概述
设计和架构究竟是什么
架构和设计:以房屋设计图为例,架构图里包含了所有的底层设计细节,这些细节共同支撑顶层架构设计,底层设计信息和顶层架构设计共同组成了整个房屋的架构文档。
目标:用最小的人力成本满足构建和维护系统的需求。
问题案例:团队扩张的同时,效率下降、变更成本的提升的问题变得愈发的明显。问题的原因:低估了良好代码、良好设计的、整洁代码的重要性。
两个价值维度
行为价值:软件产品的商业价值(利益相关者:业务部门、产品策划部门)
架构价值:软件产品的可维护、可扩展性的价值(利益相关者:技术部门)
如何权衡:从重要紧急角度来看,架构价值决定了软件未来的实现、维护、扩展成本,属于重要象限。技术团队必须能够从长期价值角度,对此进行谨慎和合理的评估,防止软件陷入难以维护、难以扩展的困境。
从基础构件开始:编程范式
面向对象
面向对象是什么:面向对象就是以多态的手段对源码中的依赖关系进行控制的能力,让底层的实现组件和高层的策略组件实现分离,能够实现独立于高层组件的开发和部署,也就是一般意义上的插件架构。
设计模式之美中的定义
面向对象是一种编程范式,以类和对象作为代码组织的最小单元,并将抽象、封装、继承、多态四大特性,作为设计和实现的基石。
OOA:面向对象分析
OOD:面向对象设计
OOP:面向对象实现
设计原则
单一职责原则 (SRP)
概念:任何一个软件模块都应该只对某一类行为者负责。
目标:为了代码的可读性、可维护性。
案例:
员工的例子,员工类(Employee)提供了三个方法
- 计算工资 calculatePay()(行为者:财务部门)
- 计算工时 reportHours()(行为者:人力部门)
- 保存到数据库 save()(行为者:技术部门、数据库、CTO)
员工类同时为三个行为者服务,违反了单一职责原则,
如何解决:
大的思路是根据职责进行拆分,拆分如下:
- 员工数据类 EmployeeData
- 工资处理类 PayCalculator
- calculatePay()
- 工时处理类 HourReporter
- reportHour()
- 数据库保存类 EmployeeSaver
- save()
并且为了方便调用,由一个类提供统一的接口给外部调用(这里使用到了外观模式)
- EmployeeFacade
- calculatePay()
- reportHour()
- save()
开闭原则(OCP)
概念:设计良好的软件应该易于扩展,同事抗拒修改。
使用设计原则:依赖的双方不依赖对方的实现,依赖对方的接口。(如果要让B的修改不影响到A,那么就让B依赖A。)。
目标:提高代码的可扩展性。
案例:报表的案例,类图如下
Interector是处理核心业务模块,不会因为其他模块的修改而受到影响。‘
Controller模块不会因为展示层Presenter(ScreenPresenter/PrintPresenter)的修改而受到影响。
里氏替换原则(LSP)
概念:子类的对象能够替换程序中父类对象出现的位置,能够保证原来程序的逻辑行为不变以及正确性不被破坏。
解读:LSP 看上去和 多态类似,但是是有本质上的区别的,多态是面向对象的原则,而LSP是关于继承的指导原则。子类的设计应该按照父类的约定,而不能违反,导致程序的二义性。从 逻辑行为 和 正确性 两个角度来看一些违反LSP的案例。
-
逻辑行为:子类重写父类的方法,但是抛出了父类未定义的异常。
-
正确性:父类定义排序方法是自然排序正序,子类重写方法修改wield了倒序或者ASCII排序,导致正确性问题。
接口隔离原则(ISP)
概念:软件模块不应该依赖没有使用到的接口。
解读:OOP层面解读,如果接口中部分方法只有被部分的模块使用,那么应该把这部分接口独立出来,提供给这部分模块使用。
目的:
- 提高易用性,接口中更少的方法意味着更容易使用,不易出错(迪米特法则)
- 提高鲁棒性,杜绝误调用了该模块中不用到的方法意外的错误出现。
控制反转原则(DIP)
概念:High-level modules shouldn’t depend on low-level modules. Both modules should depend on abstractions. In addition, abstractions shouldn’t depend on details. Details depend on abstractions.(高层模块不能依赖低层模块,两者通过抽象来互相依赖。另外抽象不能依赖具体实现,具体实现可以依赖抽象)
案例:工厂模式
- 高层模块 Application
- 低层模块 ServiceFactoryImpl/ConcreteImpl
- 底层模块的抽象 SserviceFactory/Service
组件构建原则
组件聚合
讲解以下三个组件聚合的原则
• REP: The Reuse/Release Equivalence Principle • CCP: The Common Closure Principle • CRP: The Common Reuse Principle
复用/发布等同原则(REP)
**概念:**软件复用的最小粒度应等同于其发布的最小粒度。
解读: 组件是为了解决软件复用的问题,但是在组件复用的时候我们会遇到组件版本的兼容以及使用者如何决定是否需要升级的问题。该原则指出了组件需要定义版本号以及添加对应更新日志的必要性。
- 定义版本号是为了解决复用组件的兼容问题;
- 更新日志是为了让组件使用根据这些信息作出对应的升级策略。
共同闭包原则(CCP)
概念:我们应该将那些会同时修改,并且为相同目的而修改的类放到同一个组件中,而将不会同时修改,并且不会为了相同目的而修改的那些类放到不同的组件中。
解读: 该原则是SRP原则的组件版本。两者可以简单的概括为:将相同原因和同时修改的东西放在一起,反之拆开来。
共同复用原则(CRP)
概念:不要强迫一个组件的用户依赖他们不需要的东西。
解读: 该原则是ISP原则的组件版本。两者可以简单的概括为:不要依赖没有使用到的东西。
组件聚合张力
组件的聚合张力如下所示,软件生命周期中不同的时候,会有一定的偏向性。
- 软件早期,组件依赖不多,更多考虑研发性(开发周期),所以牺牲部分复用性,为维护性而组合,偏向 CCP
- 随着项目成熟度变高,组件的依赖变多变复杂,更多的考虑软件的复用性,偏向了REP以及CRP
组件耦合
介绍解除组件耦合的一些原则
- 无环依赖原则(ADP)
- 自上而下的设计
- 稳定依赖原则(SDP)
- 稳定都想原则(SAP)
无环依赖原则(ADP)
概念:组件依赖关系中不应该出现环。
解读:循环依赖给组件的测试、复用、发布带来了很大的麻烦。可以使用DIP、创建新组建的方式解除这种循环依赖。
自上而下的设计
概念: 设计一张自上而下的组件结构图,用于指导组件的设计。
解读: 1、项目早期,对组件的闭包和可复用的组件一无所知,并且没有组件依赖的存在,组件结构图的前提条件不成熟以及存在的必要性是非必须的。2、随着项目成熟度的提高,出现了组件之间的各种依赖,有必要有一个组件结构图用于指导组件设计,(a.)防止出现组件组件之间的循环依赖、(b.)隔离稳定组件和不稳定组件。
稳定依赖原则(SDP)
概念:依赖关系要指向更稳定的方向。
解读:1、稳定组件依赖了不稳定组件,不稳定组件会难以变更,因为不稳定组件的变更都有可能影响稳定组件,可以使用DIP来修复这个问题。 2、如何量化组件的稳定程度,可以使用公式:I=fan-out/(fan-out+fan-in) (I表示不稳定性,I=0表示该组件是一个完全稳定组件、I=1表示该组件是一个完全不稳定组件;其中fan-in表示组件被依赖的次数;fan-out表示组件依赖其他的次数) 。3、可以使用DIP解除这种依赖。
使用DIP解除依赖之后的组件依赖关系
稳定抽象原则(SAP)
概念:一个组件的抽象化程度应该与其稳定性保持一致。
解读:1、SAP是让稳定组件同时具有扩展性的指导原则,可以使用OCP原则,添加稳定组件的接口和抽象类,让具体实现的组件依赖于抽象组件(符合SDP),同时也提供了扩展功能。2、组件抽象的量化方法,可以使用公式:a=Na/Nc(a表示抽象程度[0-1],a=0表示组件没有抽象,a=0表示组件完全抽象;Na表示抽象类或者接口个数;Nc表示实现类个数)。3、组件抽象程度和组件稳定程度之间的关联关系可以使用如下图形分析。(a.)处于痛苦区的稳定组件因为抽象比较少,难以扩展和修改;(b.)处于无用区的不稳定组件,抽象程度比较高,但是很少被依赖,可能是老旧代码。