欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

架构整洁之道

程序员文章站 2022-04-15 15:43:58
目标 用最少的人力成本满足构建和维护该系统的需求 目标 衡量指标 版本迭代 -- 工程师团队规模 版本迭代 -- 代码总行数 版本迭代 -- 代码变更行数 衡量指标 软件系统的价值 行为价值 按需求文档编写代码 可用性 功能性bug 性能 稳定性 紧急,但是并不总是重要,在紧急重要矩阵中占据A、C位 ......

目标

用最少的人力成本满足构建和维护该系统的需求

衡量指标

版本迭代 -- 工程师团队规模
版本迭代 -- 代码总行数
版本迭代 -- 代码变更行数

软件系统的价值

行为价值
		按需求文档编写代码
		可用性
				功能性bug
				性能
				稳定性
		紧急,但是并不总是重要,在紧急重要矩阵中占据a、c位置
架构价值
		soft :当需求变更时,所需的软件变更必须简单方便
		变更实施的难道应该和变更的范畴(scope)成等比,而与变更的具体形状(shape)无关
		不紧急,占据b、d,d的工作就是过度设计,过度设计会提升开发和维护成本

架构整洁之道

编程范式

目的 :设置限制,告诉我们不可以做什么
现有范式 :
		结构化编程 :
				目的 :对控制权的直接转移进行了限制和规范
				内容 :可以用顺序接口、分支结构、循环结构这三种结构构造出任何结构。限制goto的使用
				意义 :用代码把一些已证明的结构串联起来,就可以推导出整个程序的正确性。实际上没有办法证明每个程序段是正确的,只能证伪,如果所有的基本单原都无法证伪,那么整个就是无法证伪的,那目前就是正确的。
				延伸 :物理学与数学的区别,物理学的基本公式都是没有办法证明的,只能证伪,所以物理是实验科学,没有一个公式是完全靠得住的,只是目前靠得住。数据的基本公式都是可以证明的。
			面向对象编程 :
					目的 :对程序控制权的间接转移进行了限制和规范
					定义 :
							封装 :只暴露部分函数,数据则完全不暴露
							继承
							多态 :其实只是函数指针的一种应用,通过接口和实现,抽象类和继承,替代了函数指针的使用	
					意义 :函数指针,是跨越组件边界的方法,是组件独立部署的基础,依赖反转的基础。依赖反转指的是让依赖与控制流向相反。
			函数式编程 :
					目的 :对赋值进行了限制和规范
					趋势 :如果有足够大的存储量和计算量,应用程序可以用事件溯源的方式,用完成不可变的函数式编程,只通过事物记录,从头计算状态
					意义 :所有的竞争问题、死锁问题、并发问题都是由可变变量导致的。
					应用 :通过将状态修改的部分和不需要修改的部分分隔成单独的组件,提高系统的稳定性和效率

设计原则 :solid

意义 :
		如何将数据和函数组织成类
		如何将类链接起来成为组件和程序
内容 :
		ocp : 开闭原则
				目标 :让系统易于扩展,同时限制每次修改所影响的范围
				实现 :划分组件,并将组件间依赖关系按层次结构进行组织
				本原则是我们进行架构设计的主导原则
		srp :单一职责原则
				目标 :指导类、组件拆分
				定义 :任何一个软件模块,都应该有且只有一个被修改的原因,“被修改的原因”指系统的用户或所有者
				痛点 :同样的一块逻辑,如果服务于两个价值主体 :因为一个价值主体而修改,那么第二个价值主体期望的功能将被影响。比如cto和coo都要员工的工时,分别用于计算薪资和汇报,两者的计算方式可能目前是相同的,一方有了更改,另一方就bug了
				如果一块代码,归属于两个团队共同维护 :就会有代码合并问题		
		lsp : 里氏替换原则
				目标 :指导接口与实现方式(边界处理)
				内容 :不是实现了同一个接口,它们的行为就一致并可以互相替换,长方形正方形是典型的案例
							如果两个组件,替换之后需要分别做特别的设置,那就说明抽象的还不足够,会引入许多if-else,可以同配置清单等方式消除
		isp :接口隔离原则
				目标 :指导接口的定义(边界处理)
				内容 :不依赖任何不需要的组件、类、方法
							如果不同的用户分别使用一个大接口的几个不同的方法,那么应该把这个大接口拆分为针对这些用户的小接口
		dip : 依赖反转原则
				目标 :指导依赖方向(依赖)
				内容 :组建间跨越边界的源码依赖的方向永远与控制流的方向相反 																						

组件

定义 :是软件的部署单元,是整个软件系统可以独立完成部署的最小实体
拆分三原则 :
		rep :复用、发布等同原则 -- 内容 :软件复用的最小粒度应等同于	其发布的最小粒度
		ccp :共同闭包原则
				内容 :将为了相同目的而同时修改的类放在同一个组件中,是srp原则在组件层面的描述
				执行 :对大部分应用程序而言,可维护性的重要性远远大于可复用性
							因为一个原因需要做修改,这个修改最后在同一个组件中,如果分散在多个组件中,那么开放、提交、部署的成本都会提升
		crp :共同复用原则
				内容 :不要强迫一个组件依赖它不需要的东西,是isp原则在组件层面的描述
		张立图 :
				架构设计中有许多矛盾,研发性和复用性的矛盾,而研发性本身又有粘性(ccp)和排斥性的矛盾(crp)
				架构师做的往往是在这个张立图中找到一个最符合现在需要的点,而这个平衡也是不断变化的,根据项目的规模、迭代的节奏等。

架构整洁之道

依赖三原则 :

		无依赖环原则 :
				互相依赖的组件,实际上组成了一个大组件,这三个组件要一起发布、一起做单元测试
				通过依赖反转原则可以解依赖环
		稳定依赖原则 :
				内容 :
						依赖必须指向更稳定的方向,接口是最稳定的。
						组件的稳定性,指的是组件的变更空难度,影响因素有很多,比如代码的体量大小、复杂度、清晰度等,但最最重要的一个因素就是依赖的数量 -- 让组件难于修改的一个最直接的方法就是让很多其他组件依赖于它
				定量指标 :不稳定性(|) = 出向依赖数量 / (入向依赖数量 + 出向依赖数量)
				方法 :可以通过抽接口,共同依赖接口的方式,修正违反文档依赖的地方
		稳定抽象原则 :
				内容 :
						一个组件的抽象化程度应该与其稳定性保持一致
						为了防止高阶架构设计与高阶策略难以修改,通常抽象出稳定的接口或抽象类。越稳定的库就应该越抽象,这样它的稳定性就不会影响它的扩展性
				定量描述 :抽象程度(a) = 组件中抽象类和接口的数量 / 组件中类的数量
				将不稳定性和抽象程度分别作为横轴和纵轴,画一个二维的图,(0,1)-(1,0)连线就是主序列线。靠近(0,0)的区域是痛苦区,改动成本很大,但是又很具体。靠近(1,1)的是无用区,非常抽象,但是没有别的组件依赖它,改动成本很小,通常是废弃的。
				离主序列先的距离d = | a + | -1 | ,可以定量化的衡量一个组件的健康程度。在d满足期望的条件下,约靠近(0,1)和(1,0)越好			

软件架构

目的 :
		终极目的 :最大化程序员的生产力,最小化系统的总运营成本
		细化目的 :支撑软件系统的全生命周期,让系统便于理解、易于修改、方便维护、轻松部署			
方针 :	
		选型指的是无关紧要的细节设计
		选型例子 :
				具体选用那个存储方式,或那个数据库
						数据库 :擅长于内容的查询
						文件 :擅长于文件的快速查找和整体读取
						如果硬盘被淘汰时,用什么存储系统差别不大
				使用哪种web服务
				使用哪种框架 :
						框架的使用文档是开发者角度写的,他自然吹嘘自己能力,希望你完全耦合他们的框架
						风险 :
								产品发展,框架不再满足需求
								框架本身朝着我们不需要的方向演进
								未来我们可能希望迁到一个新的更好的框架上
			边界约完善,开发和部署成本越高,所以不完全边界能解决的,不要用完全边界,低层次解耦能解决的,不要用高层次解耦
内容 :
		组件拆分 :
				拆分 :
						水平分层 :
								一条策略距离系统的输入、输出越远,它的层次越高
								例子 :
										ui界面
										应用独有的业务逻辑
										领域普适的业务逻辑
										存储
						按用例垂直切分
								每个用例几乎涉及到所有的水平分层,如何做到新加用例,不影响旧的用例
								比如 :订单,聊天
						重复 :
								如果两段代码,看起来重复,但是走的是不同的演进路径,就不是真正的重复
				解耦模式 :
						源码层次 :做了接口、类依赖上的(不完全的)解耦,但是放在同一个组件中,通常放在不同的路径下
						部署层次 :任然运行在同一个机器上,彼此通过函数调用通讯
						服务层次 :
								运行在不同的机器上,通过url、网络数据包等方式进行通讯
								服务不等同于模块,比如横跨型变更需要改动所有服务,但是可能并不会改动架构
						从上到下,(开发、部署)成本依次升高,如果低层次的解耦已经满足需要,不要	进行高层次的解耦
				组件是一组描述如何将输入转化为输出的策略语句的集合,这些策略的变更原因、时间、层次相同		
		组件排列(依赖):依赖关系与数据流控制流脱钩,与组件所在层次挂钩。所以组件的依赖是与组件的水平分层息息相关的
				业务实体 :
						包含关键业务数据和业务逻辑
						与界面无关、与存储无关、与框架无关,只有业务逻辑,没有别的
				用例 :
						特定场景下的业务逻辑 :
								三要素 :
										需要用户提供的输入数据(注意解耦输入方式,这里只关心数据)
										用户应该得到的输出数据(注意解耦输出方式,这里只关心数据)
										从输入数据到输出数据,应该采取的处理步骤
								注意 :
										不要把业务实体直接当做输入数据对象或者输出数据对象,因为他们会以不同的原因的速率发生变更
				接口适配器 :
						整个mvc
						对存储、设备、界面等的接口声明和使用
				框架与驱动程序 :
						因为	与硬件太相关的部分,比如用户界面,是不可测的,所以这里的边界处理通常使用谦卑对象模式
						谦卑对象要有自知之明,简化到不能再简化,不应该包含对数据的任何处理。数据处理全部放到接口适配器(比如视图模型)中。
				测试层 :
						测试也是一个组件
						测试关键之外是耦合,测试如何依赖所有的其他组件的所有接口,那测试就是脆弱的,任何改动都引起n个case失效。													
						解法是给测试层单独写一套特有的api				
	组件通信 :
			方式 :
					接口调用
					服务调用
			完全边界 :
					调用双方都声明接口
					专用的输入数据类型
					专用的返回数据类型
			不完全边界
					省掉最后一步
							保留到源码层次的解耦
							声明好接口,做好分割后,任然放在一个组件中。等到时机成熟时再拆出来独立编译部署
					单向边界 :
							正常的切割,应该使用两个接口,两个雷各自使用对方的接口而不是直接使用类,但是这样的开发成本很大,所以,只实现一个接口,高层用接口调用底层,而底层直接使用高层的类
					门户模式 :
							控制权的间接转移不用接口和实现去做,而是用门户类去做,接口都不用声明了。		
	软件系统的声明周期 :
			开发 :
					不同团队负责的组件不交叉
					不使用大量复杂的脚手架
			部署 :
					减少组件数量,内部组件外部组件结合的方式
					不依赖成堆的脚本和配置文件
			运行 :
					这方面的价值对架构的影响最小
					不同吞吐量、不同的响应时长要求,是架构设计要考虑的点。采用微服务?单线程?多线程?
					架构应起到揭示系统运行的作用 :用例、功能、行为设置应该都对开发者可见的一级实体,以类、函数或模块的形式占据明显位置,
					命名能清洗地描述对应的功能				
			维护 :
					探秘成本 :
							对现有软件系统的挖掘,确定新功能或修复问题的最佳位置和方式
					风险成本 :
							做改动时,可能衍生出新的问题

架构整洁之道