可以作为设计模式的理论基础阅读,同套思想在不同层面的最佳实践。 架构解决的不是代码本身的问题,而是人力资源的问题。
概述
架构和设计没有任何区别,并非高层级的才是架构,而底层的实现细节就不是。
架构的终极目标是用最小的人力成本来满足构建和维护该系统的需求。
一个软件架构的优劣,可以用它满足用户需求所需要的成本来衡量。如果成本很低,并且在系统的整个生命周期内一直都能维持这样的低成本,那么这个系统的设计就是优良的。如果该系统的每次发布都会提升下一次变更的成本,那么这个设计就是不好的。
对于每个软件系统,我们都可以通过行为和架构两个维度来体现它的实际价值。
系统行为是紧急的;系统架构是重要的。
编程范式
详略
- 结构化编程:对程序控制权的直接转移进行了限制和规范。
- 如果代码中只采用了 if else 和 do while 两类控制结构,则一定可分解成更小、可证明的单元。
- 只要能用枚举法证明分支结构中每条路径的正确性,自然就可以推导出分支结构本身的正确性。
- 如果测试无法证伪这些函数,我们可以认为这些函数足够正确,从而推导整个程序是正确的。
- 面向对象编程:对程序控制权的间接转移进行了限制和规范。
- 函数式编程:对程序中额度赋值进行了限制和规范。
设计原则
详略
- 单一职责原则:每个模块都有且只有一个被需要修改的理由。
- 开闭原则:对扩展开放,对修改封闭,不侵入旧逻辑。
- 里氏替换原则:任何基类可以出现的地方,子类一定可以出现。
- 接口隔离原则:避免不必要的依赖关系。
- 依赖反转原则:通过依赖抽象,使得调用方(高层)不被被调用方的修改影响,接口的定义属于调用方。
组件构建原则
- 构建组件的基本原则
- REP 复用/发布等同原则:复用的最小粒度应等同于其发布的最小粒度,即一个组件的组成类和模块应该有一个共同的主题或方向。
- CCP 共同闭包原则:应该将会同时修改并且为相同目的而修改的类放在同一个组件中。
- 为 SRP 单一职责原则的组件层面的阐述。
- (原则是适用于多种维度的,仍可由代码的组件维度上升到服务的维度,当然书里说的组件本身也不是代码层面)
- CRP 共同复用原则:不要强迫一个组件的用户依赖他们不需要的东西,即将经常共同复用的类和模块放在同一组件中。
- 为接口隔离原则的普适版。不要依赖不需要的东西。
- 不可得三角:
- REP 为复用性而组合变大,CCP 为维护性组合变大,CRP 为拆分变小。
- 前期复用性低,注重 CCP,后期注重 REP。
- 稳定性指标:依赖数/(依赖数+被依赖数),越低则越稳定,即不易被其他组件修改影响。
- 抽象化指标:抽象/(抽象+实现)
- 稳定抽象原则:一个组件的抽象化程度应该和其稳定性保持一致。
- 即一个包应该由抽象(稳定性)和具体实现(不稳定)部分组成。
- 把握好这个尺度,则此组件既不会特别难以被修改,又可以实现足够的功能。
软件架构
软件架构的工作实质就是规划如何将系统切分成组件,并安排好组件之间的排列关系,以及组件之间相互通信的方式。
良好的软件架构可以让系统便于理解、易于修改、方便维护,并且能够轻松部署。终极目标就是最大化程序员的生产力,同时最小化系统的总运营成本。
- 良好的软件架构应该能明确的反映该系统在运行时的需求,但对运行的影响远不及它对开发、部署和维护的影响。
如果想设计一个便于推进各项工作的系统,其策略就是要在设计中尽可能长时间的保留尽可能多的可选项(可能性)。
- 良好的软件架构必须支持以下几点:
- 系统的业务单元和正常运行:展示系统本身,即从顶层设计上可以看出它的业务机制。
- 系统的维护
- 系统的开发
- 系统的部署
- 解耦方式:
- 显而易见的不同变更原因导致的分层解耦:界面、业务逻辑、底层服务
- 基于业务单元的垂直解耦:用例间业务逻辑不同导致的业务单元间的划分
- 这种解耦方式是为了给业务变更留出可能性,同时当被高度解耦之后也会更好的降低开发的干扰,提升部署的灵活性。
边界
- 划分边界?
- 软件架构设计本身就是一门划分边界的艺术。边界的作用是将软件分割成各种元素,以便约束边界两侧之间的依赖关系。
- 一个系统最消耗人力资源的是什么?是系统中存在的耦合。
- 边界线应该沿着系统的变更轴来画,边界线两侧的组件应该以不同原因不同速率变化。
- 系统的核心业务逻辑必须和其他组件划分边界,保持独立,这些其他组件要么可以去掉,要么有多种实现,形成插件式架构。
- 边界的存在形式
- 常见形式:
- 源码层次:文件结构组件
- 部署层级:独立的可依赖包
- 执行层次:线程、进程
- 服务层次:最强边界,网络隔离
- 大部分系统都有多种划分策略
- 常见形式:
- 策略与层次:层次间边界
- 组件应该排布为一个有向无环图,“向”为源码依赖关系
- 层次:距离输入输出越远则层次越高
- 良好的架构设计中低层组件被设计为依赖高层组件,实现于依赖反转
- 业务逻辑:核心业务边界
- 业务实体:关键业务逻辑 + 关键业务数据(领域模型??)
- 业务用例:特定应用场景下的业务逻辑,无关具体的输入输出形式
- 用例更靠近系统的输入输出,业务实体是适用于多个场景的一般化概念,所以业务实体是高层,业务用例是低层
- 业务逻辑应该保持纯粹,是系统的核心,其他低层概念应以插件的形式接入系统
- 整洁架构:边界划分理念
- 同心圆结构:
- 圆心:业务实体
- 外圈:用例
- 再外圈:接口适配器
- 最外圈:框架、驱动程序、数据库、界面
- 层次越往内,则抽象程度越高,依赖越少
- 同心圆结构:
- 展示器和谦卑对象:识别和保护系统边界
- 分割了可测试和不可测试部分(谦卑对象)。
- 谦卑对象中不包含或只包含少量的逻辑。只做为数据和不可测部分(如外部设备依赖)的桥梁。
- 不完全边界:预留的边界
- 概念:
- 架构师工作的本身需要做预见性设计
- 要把握可能出现的架构边界,评估实际的拆分成本和收益
- 可拆分而暂未拆分的边界设计
- 示例:存在显而易见的可划分的边界
- 策略模式
- 门面模式
- 概念:
- Main 组件:启动程序应是一种插件,与环境有关的启动插件
- 宏观与微观:
- 误解:
- 拆分为服务则完成了一套架构 ❌
- 服务之间是强隔离的 ❌
- 服务是支持独立开发和部署的 ❌
- 原因:
- 服务这种形式不过是跨进程、跨平台的函数调用罢了
- 服务接口和函数接口在定义上并没有什么优劣
- 还是可能会因为数据共享而强耦合(如,服务间传递的数据增加了一个字段,所有使用的服务都要修改以适配)
- 难题:横跨型变更
- 按照交互、下单、调度、派单拆分为服务的滴滴公司,某天上线了猫咪托运功能,那么所有的服务都需要修改
- 应对:基于组件的服务,服务也可以按照组件结构来部署,在添加删除组件时不影响原有逻辑
- 结论:服务边界并不能代表系统的架构边界,服务内部的组件边界才是
- 误解:
- 测试边界:略
- 整洁的嵌入式架构:硬件依赖的边界
- 避免隐形硬件依赖,避免软件部分变成固件
- 增加xx抽象层,避免了解硬件信息,屏蔽处理器、操作系统
- 实现细节:
- 数据库:数据的组织结构、模型是系统架构的重要部分但是磁盘读取机制则不是
- GUI:在软件功能整体上,UI 和业务逻辑的隔离是共识。但是前端部分也是有 UI 和前端业务逻辑的区分的
- 应用框架:面临框架选择时,可以依赖框架,但是要谨慎考虑