一、架构设计的目标
-
用最少的人力成本满足构建和维护该系统的需求
-
衡量指标
-
工程师团队规模
-
代码总行数
-
代码变更行数
-
二、软件系统的价值
-
行为价值
-
按需求文档编写代码
-
可用性
-
功能性bug
-
性能
-
稳定性
-
-
紧急,但是并不总是重要
-
-
架构价值
-
当需求变更时,所需的软件变更必须简单方便
-
变更实施的难度应该和变更的范畴(scope)成等比,而与变更的具体形状(shape)无关
-
不紧急
-
三、编程范式
-
目的:设置限制,告诉我们不可以做什么
-
现有范式
-
结构化编程
-
目的:对控制权的直接转移进行了限制和规范
-
内容:可以用顺序接口、分支结构、循环结构这三种结构构造出任何结构;限制goto的使用。
-
意义:用代码把一些已证明的结构串联起来,就可以推导出整个程序的正确性。实际上没有办法证明每个程序段是正确的,只能证伪,如果所有的基本单元都无法证伪,那么整个就是无法证伪的,那目前就是正确的。
-
延伸:物理学与数学的区别,物理学的基本公式都是没有办法证明的,只能证伪,所以物理学是实验科学,没有一个公式是完全靠得住的,只是目前靠得住。数学的基本公式都是可以证明的。
-
-
面向对象编程
-
目的:对程序控制权的间接转移进行了限制和规范
-
定义
-
封装:只暴露部分函数,数据则完全不暴露
-
继承
-
多态:其实只是函数指针的一种应用,通过接口和实现,抽象类和继承,替代了函数指针的使用
-
-
意义:函数指针,是跨越组件边界的方法,是组件独立部署的基础,依赖反转的基础。依赖反转指的是让依赖与控制流向相反。
-
-
函数式编程
-
目的:对赋值进行了限制和规范
-
趋势:如果有足够大的存储量和计算量,应用程序可以用事件溯源的方式,用完全不可变的函数式编程,只通过事务记录,从头计算状态
-
意义:所有的竞争问题、死锁问题、并发问题都是由可变变量导致的。
-
应用:通过将状态修改的部分和不需要修改的部分分隔成单独的组件,提高系统的稳定性和效率
-
-
四、设计原则
-
意义
-
如何将数据和函数组织成类
-
如何将类链接起来成为组件和程序
-
-
开闭原则(OCP)
-
目标:让系统易于扩展,同时限制每次修改所影响的范围
-
实现:划分组件,并将组件间依赖关系按层次结构进行组织
-
本原则是我们进行架构设计的主导原则
-
-
单一职责原则(SRP)
-
目标:指导类、组件拆分
-
定义
-
任何一个软件模块,都应该有且只有一个被修改的原因
-
“被修改的原因”指系统的用户或所有者
-
-
痛点
-
同样的一块逻辑,如果服务于两个价值主体,因为一个价值主体而修改,那么第二个价值主体期望的功能将被影响。比如CTO和COO都要员工的工时,分别用于计算薪酬和汇报,两者的计算方式可能目前是相同的,一方有了更改,另一方就bug了
-
如果一块代码,归属于两个团队共同维护,就会有代码合并问题
-
-
-
里氏替换原则(LSP)
-
目标:指导接口与实现方式【边界处理】
-
内容
-
不是实现了同一个接口,它们的行为就是一致并可以互相替换,长方形正方形是典型的案例
-
如果两个组件,替换之后需要分别做特别的设置,那就说明抽象得还不足够,会引入许多if-else,可以配置清单等方式消除
-
-
-
接口隔离原则(ISP)
-
目标:指导接口的定义【边界处理】
-
内容
-
不依赖任何不需要的组件、类、方法
-
如果不同的用户分别使用一个大接口的几个不同方法,那么应该把这个大接口拆分为针对这些用户的小接口
-
-
-
依赖反转原则(DIP)
-
目标:指导依赖方向【依赖】
-
内容:组件间跨越边界的源码依赖的方向永远与控制流的方向相反
-
五、组件
-
定义:是软件的部署单元,是整个软件系统可以独立完成部署的最小实体
-
拆分原则
-
REP:复用、发布等同原则
-
内容:软件复用的最小粒度应等同于其发布的最小粒度
-
-
CCP:共同闭包原则
-
内容:将为了相同目的而同时修改的类放在同一个组件中,是SRP原则在组件层面的描述
-
执行
-
对大部分应用程序而言,可维护性的重要性远远大于可复用性
-
因为一个原因需要做修改,这个修改最好在同一个组件中,如果分散在多个组件中,那么开发、提交、部署的成本都会上升
-
-
-
CRP:共同复用原则
-
内容:不要强迫一个组件依赖它不需要的东西,是ISP原则在组件层面的描述
-
-
张力图
-
-
架构设计中有许多矛盾,研发性和复用性的矛盾,而研发性本身又有粘性(CCP)和排斥性(CRP)的矛盾
-
架构师做的往往是在这个张力图中找到一个最符合现在需要的点,而这个平衡也是不断变化的,根据项目的规模、迭代的节奏等
-
-
-
依赖原则
-
无依赖环原则
-
互相依赖的组件,实际上组成了一个大组件,这三个组件要一起发布、一起做单元测试
-
通过依赖反转原则可以解依赖环
-
-
稳定依赖原则
-
内容
-
依赖必须指向更稳定的方向,接口是最稳定的
-
组件的稳定性,指的是组件的变更困难度,影响因素有很多,比如代码的体量大小、复杂度、清晰度等,但是最重要的一个因素就是依赖的数量——让组件难于修改的一个最直接的办法就是让很多其它组件依赖与它
-
组件的稳定性和它的变更频繁度没有直接的关联。或者说,稳定性可以分为价值(需求)的稳定性,和组件自身的稳定性
-
-
定量指标:不稳定性(I)=出向依赖数量/(入向依赖数量 + 出现依赖数量)
-
方法:可以通过抽接口,共同依赖接口的方式,修正违反稳定依赖的地方
-
-
稳定抽象原则
-
内容
-
一个组件的抽象化程度应该与其稳定性保持一致
-
为了防止高阶架构设计和高阶策略难以修改,通常抽象出稳定的接口或抽象类。越稳定的库就应该越抽象,这样它的稳定性就不会影响它的扩展性
-
-
定量描述
-
抽象程度(A)=组件中抽象类和接口的数量/组件中类的数量
-
将不稳定性和抽象程度分别作为横轴和纵轴,画一个二维的图,(0,1)-(1,0)连线就是主序列线。靠近(0,0)的区域是痛苦区,改动成本很大,但是又很具体。靠近(1,1)的是无用区,非常抽象,但是没有别的组件依赖它,改动成本很小,通常是废弃的。
-
离主序列线的距离D=|A+I-1|,可以定量化的衡量一个组件的健康程度。在D满足期望的条件下,越靠近(0,1)和(1,0)越好
-
-
-
六、封装方式
-
经典的封装方式
-
按层封装
-
问题
-
无法展现具体的业务领域
-
不能防止跨层调用。通常情况下,绕过业务逻辑是不合理的,尤其是要控制权限时
-
-
-
按功能封装
-
端口和适配器
-
按组件封装
-
-
组织形式与封装的区别
-
如果没有封装和隐藏功能,采用任何架构风格都没有区别
-
七、软件架构
-
定义:架构是有关软件整体结构与组件的抽象描述,用于指导大型软件系统各个方面的设计
-
目的
-
终极目的:最大化程序员的生产力,最小化系统的总运营成本
-
细化目标:支撑软件系统的全生命周期,让系统便于理解、易于修改、方便维护、轻松部署
-
-
方针
-
尽可能长时间地保留尽可能多的可选项
-
选项指的是无关紧要的细节设计
-
选项例子
-
具体选用哪个存储方式,或哪个数据库
-
使用哪种web服务
-
使用哪种框架
-
-
-
边界越完善,开发和部署成本越高。所以不完全边界能解决的,不要用完全边界;低层次解耦能解决的,不要用高层次解耦
-
-
内容
-
组件拆分
-
切分
-
水平分层
-
按用例垂直切分
-
重复性
-
-
解耦模式
-
源码层次
-
部署层次
-
服务层次
-
从上到下,开发、部署成本依次升高,如果低层次的解耦已经满足需要,不要进行高层次的解耦
-
-
-
组件排列(依赖)
-
依赖关系与数据流控制流脱钩,与组件所在层次挂钩。所以组件的依赖是与组件的水平分层息息相关的
-
分层
-
业务实体
-
用例
-
接口适配器
-
框架与驱动程序
-
测试层
-
-
-
组件通信
-
方式
-
接口调用
-
服务调用
-
-
完全边界
-
调用双方都声明接口
-
专用的输入数据类型
-
专用的返回数据类型
-
-
不完全边界
-
省掉最后一步:保留到源码层次的解耦;声明好接口,做好分割后,仍然放在一个组件中,等到时机成熟时再拆出来独立编译部署
-
单向边界:正常的切割,应该使用两个接口,两个类各自使用对方的接口,而不是直接使用类,但是这样的开发成本很大,所以,只实现一个接口,高层用接口调用低层,而低层直接使用高层的类
-
门户模式:控制权的间接转移不用接口和实现去做,而是用门户类去做,接口都不用声明了
-
-
-
-
软件系统的生命周期
-
开发
-
不同团队负责的组件不交叉
-
不使用大量复杂的脚手架
-
-
部署
-
减少组件数量,内部组件外部组件结合的方式
-
不依赖成堆的脚本和配置文件
-
-
运行
-
这方面的价值对架构的影响最小
-
不同吞吐量、不同的响应时长要求,是架构设计要考虑的点
-
架构应起到揭示系统运行的作用:用例、功能、行为设置应该都是对开发者可见的一级实体,以类、函数或模块的形式占据明显位置,命名能清晰地描述对应的功能
-
-
维护
-
探秘成本:对现有软件系统的挖掘,确定新功能或修复问题的最佳位置和方式
-
风险成本:改动时,可能衍生出新的问题
-
-
参考资料
-
https://www.jianshu.com/p/d92c41c3c7b0
微信公众号“前端那些事儿”