设计架构究竟是什么
两个价值维度
行为价值
按照需求文档编码,调试解决问题
系统行为是紧急,单不总是特别重要
架构价值
系统架构是重要的,但不是特别紧急
艾森豪威尔 矩阵
紧急和重要
紧急的永远是不重要的
重要的永远不是紧急的
平衡系统架构重要性,与功能的紧急程度这件事,是软件研发人员自己的职责
三个编程范式
范式:即程序的编写模式
结构化编程
结构化编程对程序控制权的直接转移进行了限制和规范
将程序分解为一些列可证明的函数,如果这些函数无法被证伪那么说明这个函数是趋于正确的
功能性降级拆分任然是最佳实践之一
面向对象编程
面向对象编程对程勋控制权的间接转移进行了限制和规范
什么是面向对象
数据与函数的组合
一种对真实世界的建模方式
封装,继承,多态
封装
把一组相关联的数据和函数圈起来,是的圈外只能看到部分函数,数据时完全不可见的
C 是完美封装的体现,C++ Java C#都是对封装的削弱
继承
在某个作用域内对外部定义的莫一组变量与函数进行覆盖
多态
就是函数指针的一种应用,没有创新,但是变得更安全,便于使用
依赖翻转
源码上的依赖关系继承,但是这个继承与控制流相反,因此成为依赖翻转
通过依赖反转,实现面向对象编程的多态,改变源码依赖关系和控制流的同向关系
面向对象编程就是以多态为手段对源代码中的依赖关系进行控制的能力
原始初衷是软件复用
函数式编程
函数式编程对程序中的赋值进行了限制和规范
函数编程强烈依赖于λ演算
软件架构三大关注点
功能性
组件独立性
数据独立
设计原则
SOLID
SRP-单一职责原则
每个软件模块有且只有一个被修改的理由
任何一个软件模块都应该只对某一类行为者负责
组要讨论的是函数和类质检的关系
在组件层面,可以称为共同闭包原则
在软件架构层面,用于奠定架构边界的变更轴心
OCP-开闭原则
可以增加代码来修改系统行为,二非只能靠修改原来的代码
设计良好的计算机软件应该易扩展,同时抗拒修改
LSP-里氏替原则
如果想要可替换的组件来构建软件系统没那么这些组件必须遵守同一个约定
父类与子类的继承原则
ISP-接口隔离原则
软件设计的过程中避免不必要的依赖
任何层次的软件设计如果依赖于不需要的东西都是有害的
源码层来说这样的依赖关系会导致必要的重新编译和部署
DIP-依赖翻转原则
高层次的代码不应该依赖实现底层细节的代码,相反的实现底层细节的代码应该依赖于高层次策略性的代码
在源码层次的依赖关系中就应该多引用抽象类型,而非具体的实现
设计稳定的抽象层
1、代码中多食用抽象接口,避免使用那些多变的具体实现类
2、不要在具体实现类上创建衍生类
3、不要覆盖包含具体实现的函数
4、应避免在代码中写入与任何具体实现相关的名字,或者是其他容易变动的事物的名字
工厂模式
抽象工厂模式
源代码依赖的方向永远是控制流方向的反转,这就是DIP被称为依赖反转原则的原因
SOLID 原则是用来帮组我们定义软件架构中的组件和模块的
软件构建中层结构的主要目标
软件可容忍被改动
软件容易被理解
构建可在多个软件系统中复用组件
组件构建原则
什么是组件
软件的部署单元,部署过程中可以独立完成部署的最小单元
编译语言中,是一组二进制文件集合
解释运行语言中,是一组要源代码文件集合
设计良好的组件都应该永远保持可悲独立部署的特性
组件的发展史
划分地址段,加载不同的代码块
重定位技术
二进制文件,能够任意被加载到任意内存地址
连接器
外部引用与外部定义连接
将程序切分成多个可被分别编译,加载的程序段
程序的规模增加,导致连接器效率开始变低
输入:将加载过程和连接过程进行分离,耗时长的部分为连接部分,放到一个单独的程序中进行,这个程序就是连接器
输出:已经完成了外部连接的,可以重定位的二进制文件
程序规模墨菲定律:程序规模会一直不断增长下去,知道将优先的编译和链接时间填满为止
组件聚合
关注的是源码之间的组合
三个原则
REP:复用/发布等同原则
软件设计,架构设计维度,原则就是指组件中的类与模块必须是彼此紧密相关的
软件复用的最小粒度应等同于其发布的最下粒度
明确的发布版本号
保证组件之间能够彼此兼容
组件发布的时间,以及每次发布带来了那些变更
该原则的薄弱性会由 CCP,CRP进行补偿
为复用性二组合
CCP:共同必保原则
作用在类的层面-> 体现在组件上
同时修改,相同修改原因的类放到同一组件中,相反的放在同一组件中
是SRP原则在组件层面的在读阐述
大部分应用来说,可维护性重要性远远高于可复用性
程序的变更,最好控制在同一组件中,控制变更范围,方便验证部署
CCP讨论的就是OCP中的闭包,CCP原则是便于扩展,抗拒修改
为维护性而组合
CRP:共同复用原则
不要强迫一个组件的用户依赖他们不需要的东西
用于决策类和模块归属于哪一个组件的原则
不是紧密相连的类不应该被放在同一组件中
CRP原则是ISP原则的一个普适版
CRP 作用组件, ISP作用在函数类
为避免不必要的发布而且分
组件聚合张力图
REP与CCP 是粘合性原则
是组建变得更大
导致太多不必要的发布
CRP与CRP
排除性原则
复用困难
REP与CRP
太多组件变更
项目早期,CCP与REP跟重要, 速度比复用性跟重要
项目组件结构设计上的中心是根据该项目的开发时间和成熟度不断的变动的
组件耦合
描述的是组件之间的关系
影响组件结构不仅有技术水平,和公司内部政治斗争两个因素,还有结构本身是不断变化的
无依赖环原则(ADP)
组件依赖关系图中不应该出现环
每周构建
消除循环依赖
将研发项目划分为一些可单独发布的组件,交个担任或者一组程序员独立粒完成
打破循环依赖
将依赖图转换为DAG图,有向无环图
应用依赖返转原则
创建一个新的组件
产生需求变更,组件结构也会不停的抖动和扩张
自上而下的设计
组件结构不可能自上而下的设计出来的
组件依赖结构图并不是描述应用程序功能的,而是应用程序在构建性与维护性方面的一张地图
组件关系必须要随着项目的逻辑设计一起扩张和演进
稳定依赖原则(SDP)
依赖关系必须要指向更稳定的方向
稳定性应该与变更的频繁度没有直接关系,而与变更所需的工作量有关
没有任何出度依赖关系的组件是独立组件
稳定性指标
Fan-in 入向依赖 别人依赖自己的
fan-out 出向依赖 自己依赖别人的
I 不稳定性 = Fan-out/ fan-in + Fan -out I = [0-1] I=0 最稳定,自己不依赖别人, I=1 最不稳的,全都依赖别人
并不是所有组件都应该是最稳定的
违反了SDP原则可以使用DIP原则来修复
SAP 稳定抽象原则
一个组件的抽象程度应该与其稳定性保持一致
依赖关系应该指向更抽象的方向
高阶策略-应该被放到稳定组件中
使用OCP 可以让一个无限稳定的组件接收变更--> 抽象类
稳定的组件同时应该是抽象的,这样他的稳定性不会影响到他的可扩展性 -> 由接口和抽象类组成
SDP与SAP组合=组件上的DIP
衡量抽象画程度
Nc组建中类的数量
Na 组建中抽象类的数量
A 抽象程度= Na/Nc
主序列
主序列线 即 A(0,1) 与I(1,0) 的连线
组件能出与最优的位置是线的两端
痛苦区
(0,0) 区域为痛苦区,稳定,但是不抽象
无用区
(1,1)附近为无用区, 五无限抽象,但是没有被别人依赖
对于多变的组件应该远离上面两个区域
离主序列线的距离
D指标 = |A+I-1|
D=0 在主序列上
D=1 远离主序列上
D指标可以量化一个系统设计与主序列的契合程度
良好的设计,D指标的平均值和方差都应该接近于0
架构
架构的特点
设计就是架构 二者没有任何区别
架构是高层级排除底层
软件架构的终极目标是,用最小的人力成本满足构建和维护系统的需求
底层设计细节和高层架构信息不可分割,共同定义了整个软件系统
设计注重底层结构
乱麻系统的特点:没有经过设计,匆匆忙忙被构建起来
好的架构
目的
为了在工作中根号的对这些组件进行研发,部署,运行以及维护
目标
最高优先级目标是保持系统的正常工作
主要目标是,支撑软甲系统的全生命周期
终极目标是最大化程序员生产力,最小化系统的总运营成本
良好架构设计有
便于理解
易修改
轻松部署
计的目标就是实现"立刻部署”
方便维护
支持用例与正常运行
软件架构必须为其用例提供支持
开发
一个有多个不通目标团协作开发的系统必须就有相应的软件架构
保留可选项
解耦模式
源码层次
单体结构
所有组件都会在同一个地址空间内执行
通过简单的函数调用进行彼此交互
运行时作为一个执行文件被同意加载到计算机内存中
部署层次
二进制层次
大部分组件都会在同一个地址空间内执行,
通过函数调用进行彼此交互
也有别的组件运行在同一处理器下的别的进程,需要快进程通信
服务层次
执行单元层次
一个好的系统所适用的解耦模式可能会随着时间的变化而变化,不是一层不变的,应该允许从单体,到一组可独立部署的单元,到独立的服务或者微服务,还能退回到单体
整洁架构
尖叫的软件架构
架构设计不是与框架相关的,框架知识一个工具和手段
良好的架构设计应该只关注用例,并能将他们与其他的周边因素隔离
web 是一种交付手段,一种IO设备,一种实现细节
框架,是一种工具,避免让框架主导我们的架构设计
可测试的架构设计
围绕用例展开
用例调度业务实体对象
确保所有测试都不需要依赖框架
整洁架构
目前被提出来的架构:
六边形架构
DCI架构
BCE架构
共同的特点:独立于框架, 可被测试,独立于UI,独立于数据库,独立于任何外部机构
依赖关系规则
web,device,DB,GUI, EXI-> GW,CR,View-> case -> 业务entity
靠近圆心,其所在的软件层次就越高
外层圆代表的是机制,内层代表的是策略
源码中的依赖关系必须指向同心圆的内层,有底层机制,指向高层策略
不能让外层圆中发生的任何变更,影响到内层圆的代码
实体
用例
接口适配器
GUI, EXI
框架与驱动程序
web,device,DB
源码层面的依赖关系一定要指向同心圆的内侧
层次越往内,其抽象和策略的层次越高,同时软件的抽象程度就越高,其包含的高层策略就越多
最内层的圆中包含的是最通用、最高层的策略,最外层的圆包含的是最具体的实现细节
展示器与谦卑对象
谦卑对象模式
展示器实际上是采用谦卑对象模式的一种形式
设计的最初目的是帮组单元测试编程这区分容易测试行为和难以测试的行为,并将其隔离
拆分为两组模块或者类
前谦卑组,包含了系统中难以测试的行为
不属于谦卑对象的行为
GUI
展示器 易测试
视图 难以测试
测试与架构
强大的可测试 是系统设计优良的一个衡量标准
数据库网关,多态的接口
用例交互器
数据库中间的组件
数据映射器属于数据库层
ORM,将数据从数据库加载到了对应的数据结构中
是数据库,与数据库网关接口构建的另一种谦卑对象边界
服务监听器
快便捷的通信肯定需要用到魔种简单的数据结构,而边界自然会将系统分割成难以测试和容易测试的部分,在边界出使用谦卑对象模式,可以大幅提高整个系统的可测试性
不完全边界
大量的前期工作,和后期维护工作,架构师需要预留“后路”
组件再构建
先分割成独立可编译,独立部署的组件,在构建成一个组件
单项边界
策略模式
门户模式
服务列表不可见,由门户进行转发
层次与边界
架构边界可能存在与任何地方
架构师需要聪明一点,仔细权衡成本,系统什么地方需要设计架构边界
Main组件
特点
最细节化的部分,也是底层的策略,只有操作系统会依赖于它
处于整洁架构的最外圈
应用抽象的一个插件
以插件的形式存在,所以一个系统可以有多个Main组件,各自对于不通的配置
功能
负责创建,协调,监督其他组件的运作的组件
任务:创建所有的工程类,策略类,以及其他全局设置,最终将系统的控制权转交给最高抽象层的代码来处理
如何产生
依赖注入框架,注入
边界
划分边界
软件设计本身就是一门划分边界的艺术
边界的作用就是将入软件分割成各种元素,以便约束边界两边的依赖关系
架构师最前的目标是最大限度的降低构建和维护一个系统所需要的人力资源
系统中最消耗人力的是耦合
不成熟的决策
决策与系统的业务需求无关
包括:采用的框架,数据库,Web服务器,工具库,依赖注入等
设计良好的架构
细节性的决策都应该是辅助性的,可以被推迟
不需要依赖细节,应该推迟这些细节性的决策,降低这些推迟的细节对系统的影响
边界线应该划在那些不相关的事情中间
边界划分的原则
I/O是无关紧要的
插件式架构
可插拔
插件可以随时替换
边界线应该沿着系统的变更轴划分,位于边界两侧的组件应该以不同的原因,不通的速率变化这,互不影响
单一职责原则能够告诉我们应该在哪里划分边界
划分边界步骤
1、将系统分割成组件
核心业务逻辑组件
与核心业务逻辑无关但负责提供必要功能的插件
修改源代码,让非核心业务逻辑组件依赖于核心业务组件逻辑组件
即DIP和SAP 的具体应用
依赖箭头方向,应该由底层具体实现细节指向高层抽象的方向
边界剖析
跨边界调用
就是边界线一侧调用另外一侧的函数
边界划分就是在模块之间建立边界之间源码变更的影响范围的防火墙
单体结构
同一函数、统一地址他空间内的函数和数据进行某种划分
最后产生了一个单独的可执行文件
利用动态的多态来管理器内部的依赖关系
组件之间的交互一般情况下都只是普通的函数调用,迅速而廉价,,跨源码层次解耦边界的通信会很频繁
部署层次的组件
最常见的物理边界形式:动态链接库
所有函数仍然处于统一进程,统一地址空间中
组件之间的跨边界调用也只是普通的函数调用,成本很低
线程
不属于架构边界,也不属于部署单元
一个线程可以包含在单一的组件中们也可以横跨多个组件
本地进程
明显的物理边界形式
拥有不同的地址空间,
通过socket 通信
这种架构设计的目标是让低进程成为高进程的一个插件
服务
系统架构中最强的边界形式就是服务
一个服务就是一个进程
服务之间跨边界的调用是通过网络来实现与相比函数调用的,速度缓慢
无论是服务还是本地进程,几乎肯定都是用一个或者多个源码组件组成的单体架构,或者一组动态连接的可部署组件
系统中会同时包含,高通信量,低延时的本地架构边界和 低通信,高延时的服务边界
策略与层次
软件系统都是一组策略语句的集合
原则:按照变更的方式进行重新分组
变更原因、时间和层次相同的策略应该被分到同一个组件中
变更原因、时间和层次不同的策略则应该分属于不同的组件
依赖关系的方向通常取决于它们所关联的组件层次
低层组件被设计为依赖于高层组件
业务逻辑
业务逻辑就是程序中那些真正用于赚钱或省钱的业务逻辑与过程
业务实体
关键业务逻辑和关键业务数据是紧密相关的,很适合被放在同一个对象中处理
用例
本质上就是关于如何操作一个自动化系统的描述,它定义了用户需要提供的输入数据、用户应该得到的输出信息以及产生输出所应该采取的处理步骤
不是非业务实体所包含的关键业务逻辑
控制着业务实体之间的交互方式
与数据流入流出系统的方式无关
业务实体是高层次概念,业务实体是低层次概念
用例描述的特定的业务场景
实体是可以使用于多个应用场景的一般化概念
请求和响应模型
业务实体与请求对象或者响应对象进行耦合,或者是请求响应对象引用业务实体都是对CCP和SRP 原则的违反,因为他们可能一不通的原因和速率发生变更
业务逻辑应该是系统中最独立,复用性最高的代码
服务
宏观与微观
面向服务的架构
服务之间视乎是强隔离的,并不完全是这样
服务被认为是支持独立开发和部署的,并不完全是这样
架构设计的任务:
找到高层策略与低层细节之间的架构边界,同时保证这些边界遵守依赖关系规则
服务
本身只是一种比函数调用方式成本稍高的,分割应用程序行为的一种形式,与系统架构无关
是一种跨进程/平台边界的函数调用而已
按功能切分服务的架构方式,在跨系统的功能变更时是最脆弱的
基于组件的服务
横跨性变更
系统的架构边界事实上并不落在服务之间,而是穿透所有服务,在服务内部以组件的形式存在
服务内部采用遵守依赖关系原则的组件设计方式
服务边界并不能代表系统的架构边界,服务内部的组件边界才是
系统的架构是由系统内部的架构边界,以及边界之间的依赖关系所定义的,与系统中各组件之间的调用和通信方式无关
测试边界
测试也是一种系统组件
测试组件是可以独立部署的
大部分测试组件都是被部署在测试环境中,而不是生产环境中的
可测试性设计
脆弱的测试还往往会让系统变得非常死板
就是不要依赖于多变的东西
测试专用API
专门为验证业务逻辑的测试创建一个API
应该被授予超级用户权限
允许测试代码可以忽视安全限制
绕过那些成本高昂的资源(
强制将系统设置到某种可测试的状态中
结构性耦合
测试代码所具有的耦合关系中最强大、最阴险的一种形式。
安全性
将测试专用API及其对应的具体实现放置在一个单独的、可独立部署的组件中。
整洁的嵌入式架构
软件(software)应该是一种使用周期很长的东西
固件(firmware)则会随着硬件演进而淘汰过时
固件通常被存储在非可变内存设备,例如ROM、EPROM或者闪存中。
固件是直接编程在一个硬件设备上的一组指令或者一段程序
固件是嵌入在一个硬件中的软件程序
固件是被写入到只读内存设备中的(ROM)程序或数据。
软件构建过程中的三个阶段
先让代码工作起来
然后再试图将它变好
最后再试着让它运行得更快
可测试的嵌入式架构
分层
软件
固件
硬件
是实现细节
软件与固件之间的边界为硬件抽象层
软件与操作系统之间的边界为操作系统抽象层
实现细节
数据库只是实现细节
数据模型很重要
数据库始终是一种工具,也是一种实现细节
为了应对硬盘访问速度带来的限制,必须使用索引、缓存以及查询优化器等技术
文件系统与关系型数据库
文件系统关注文件名
数据库关注内容
Web 是一种实现细节
GUI只是一个实现细节。而Web则是GUI的一种,所以也是一个实现细节。
应用程序框架是实现细节
解决问题可能和其他人遇到的大体上一致
风险
框架自身的架构设计很多时候并不是特别正确的
框架可能会帮助我们实现一些应用程序的早期功能,但随着产品的成熟,功能要求很可能超出框架所能提供的范围
框架本身可能朝着我们不需要的方向演进
未来我们可能会想要切换到一个更新、更好的框架上
解决方案
将框架作为架构最外圈的一个实现细节来使用,不要让它们进入内圈
设计指导
按层封装
Web代码分为一层,业务逻辑分为一层,持久化是另外一层
水平分层,相同类型的代码在一层
每一层只能对相邻的下层有依赖关系
分层架构无法展现具体的业务领域信息
分层架构设计的目的是将功能相似的代码进行分组
按功能封装
垂直切分,根据相关的功能、业务概念或者聚合根(领域驱动设计原则中的术语)来切分
在常见的实现中,所有的类型都会放在一个相同的包中,以业务概念来命名
子主题 3
端口和适配器
“端口和适配器”“六边形架构”“边界、控制器、实体”
创造出一个业务领域代码与具体实现细节(数据库、框架等)隔离的架构
可以区分出代码中的内部代码(领域,Domain)与外部代码(基础设施,Infrastructure)。
按组件封装
是将一个粗粒度组件相关的所有类放入一个Java包中
以一种面向服务的视角来构建软件系统,与微服务架构类似
像端口和适配器模式将Web视为一种交付手段一样
“按组件封装”将UI与粗粒度组件分离
组件的定义
在一个执行环境(应用程序)中的、一个干净、良好的接口背后的一系列相关功能的集合
单体程序中的一个良好定义的组件,是微服务化架构的一个前提条件。
具体实现细节中的陷阱
子主题 1
组织形式与封装的区别
其他的解耦合模式