架构整洁之道

第1章 设计和架构

相同的概念

第2章 两个价值维度

行为价值、架构价值
软件要软
好的系统架构设计应该尽可能做到与“形状”无关。
究竟是系统行为更重要,还是系统架构的灵活性更重要?哪个价值更大?系统正常工作更重要,还是系统易于修改更重要?
美国前总统艾森豪威尔的紧急/重要矩阵

第2部分 从基础构件开始:编程范式

第3章 编程范式总览

结构化编程(structured programming)
Dijkstra于1968年最先提出
结构化编程对程序控制权的直接转移进行了限制和规范

面向对象编程(object-oriented programming)
1966提出,比结构化编程还早了两年。
面向对象编程对程序控制权的间接转移进行了限制和规范。

函数式编程(functionalprogramming)。
三个范式中最先被发明的,近些年才刚刚开始被采用
函数式编程对程序中的赋值进行了限制和规范。

每个编程范式的目的都是设置限制,三个编程范式分别限制了goto语句、函数指针和赋值语句的使用。
三个编程范式都是在1958年到1968年这10年间被提出来的,后续再也没有新的编程范式出现过。

第4章 结构化编程

Bohm和Jocopini证明了人们可以用顺序结构、分支结构、循环结构这三种结构构造出任何程序。
Dijkstra展示了顺序结构的正确性可以通过枚举法证明
Dijkstra利用枚举法又证明了分支结构的可推导性
Dijkstra需要采用数学归纳法证明循环结构
老的编程语言(类似FORTRAN和COBOL)中的完全无限制的goto语句
支持goto关键词的编程语言也通常限制了goto的目标不能超出当前函数范围。
Dijkstra曾经说过“测试只能展示Bug的存在,并不能证明不存在Bug”
通过无法证伪来证明软件的正确性。

第5章 面向对象编程

5.3 多态
程序应该与设备无关
系统行为决定了控制流,而控制流则决定了源代码依赖关系。
源代码依赖关系都可以通过引入接口的方式来进行反转。
可以用它来让数据库模块和用户界面模块都依赖于业务逻辑模块,而非相反。
让用户界面和数据库都成为业务逻辑的插件
也就是说,业务逻辑模块的源代码不需要引入用户界面和数据库这两个模块。
如果系统中的所有组件都可以独立部署,那它们就可以由不同的团队并行开发,这就是所谓的独立开发能力。
插件式架构,让高层策略性组件与底层实现性组件相分离,底层组件可以被编译成插件,实现独立于高层组件的开发和部署。

第6章 函数式编程

6.3 事件朔源
存储和处理能力的大幅进步
只存储事务记录,不存储具体状态。当需要具体状态时,只要从头开始计算所有的事务即可。
这种数据存储模式中不存在删除和更新的情况,我们的应用程序不是CRUD,而是CR。
因为更新和删除这两种操作都不存在了,自然也就不存在并发问题。
如果我们有足够大的存储量和处理能力,应用程序就可以用完全不可变的、纯函数式的方式来编程。
我们过去50年学到的东西主要是——什么不应该做。

第3部分 设计原则

第7章 SRP:单一职责原则

曾经这样描述SRP:任何一个软件模块都应该有且仅有一个被修改的原因。
也可以这样描述SRP:任何一个软件模块都应该只对一个用户(User)或系统利益相关者(Stakeholder)负责。
SRP的最终描述:任何一个软件模块都应该只对某一类行为者负责。

第8章 OCP:开闭原则

将系统划分为一系列组件,并且将这些组件间的依赖关系按层次结构进行组织,使得高阶组件不会因低阶组件被修改而受到影响。

第9章 LSP:里氏替换原则

第10章 ISP:接口隔离原则

任何层次的软件设计如果依赖于不需要的东西,都会是有害的。

第11章 DIP:依赖反转原则

如果想要设计一个灵活的系统,在源代码层次的依赖关系中就应该多引用抽象类型,而非具体实现。

11.1 稳定的抽象层
每次修改抽象接口的时候,一定也会去修改对应的具体实现。
但反过来,当我们修改具体实现时,却很少需要去修改相应的抽象接口。
所以我们可以认为接口比实现更稳定。

该设计原则归结为以下几条具体的编码守则:
应在代码中多使用抽象接口,尽量避免使用那些多变的具体实现类。
不要在具体实现类上创建衍生类。
不要覆盖(override)包含具体实现的函数。
应避免在代码中写入与任何具体实现相关的名字,或者是其他容易变动的事物的名字。

11.1.1 工厂模式
抽象层与具体实现层的边界。
所有跨越这条边界源代码级别的依赖关系都应该是单向的,即具体实现层依赖抽象层。
将整个系统划分为两部分组件:抽象接口与其具体实现。
抽象接口组件中包含了应用的所有高阶业务规则,而具体实现组件中则包括了所有这些业务规则所需要做的具体操作及其相关的细节信息。
控制流跨越架构边界的方向与源代码依赖关系跨越该边界的方向正好相反,
源代码依赖方向永远是控制流方向的反转——这就是DIP被称为依赖反转原则的原因。

第4部分 组件构建原则

第12章 组件

第13章 组件聚合

第14章 组件耦合

14.1 稳定抽象原则
一个组件的抽象化程度应该与其稳定性保持一致。

第5部分 软件架构

第15章 什么是软件架构

软件架构这项工作的实质就是规划如何将系统切分成组件,并安排好组件之间的排列关系,以及组件之间互相通信的方式。
设计软件架构的目的,就是为了在工作中更好地对这些组件进行研发、部署、运行以及维护。

15.1 开发(Development)
系统架构的作用就是要方便其开发团队对它的开发。
不同的团队结构应该采用不同的架构设计。
一方面,对于一个只有五个开发人员的小团队来说,可非常高效地共同开发一个没有明确定义组件和接口的单体系统(monolithic system)。
另一方面,如果一个软件系统是由五个不同的团队合作开发的,而每个团队各自都有七个开发人员的话,
不将系统划分成定义清晰的组件和可靠稳定的接口,开发工作就没法继续推进。
通常,如果忽略其他因素,该系统的架构会逐渐演变成五个组件,一个组件对应一个团队。(组织架构决定了软件架构)

15.2 部署(Deployment)
实现一键式的轻松部署应该是我们设计软件架构的一个目标。
微服务架构的组件边界清晰,接口稳定,非常利于开发。
但当我们实际部署这种系统时,就会发现其微服务的数量已经大到令人望而生畏,
而配置这些微服务之间的连接以及启动时间都会成为系统出错的主要来源。
有意地减少微服务的数量,采用进程内部组件与外部服务混合的架构,以及更加集成式的连接管理方式。

15.3 运行(Operation)
架构应该起到揭示系统运行过程的作用。

15.4 维护(Maintenance)
将系统切分为组件,并使用稳定的接口将组件隔离,
我们可以将未来新功能的添加方式明确出来,并大幅度地降低在修改过程中对系统其他部分造成伤害的可能性。

15.5 保持可选项
让软件维持“软”性的方法就是尽可能长时间地保留尽可能多的可选项。
所有的软件系统都可以降解为策略与细节这两种主要元素。
策略体现的是软件中所有的业务规则与操作过程,因此它是系统真正的价值所在。
细节则是指那些让操作该系统的人、其他系统以及程序员们与策略进行交互,但是又不会影响到策略本身的行为。
它们包括I/O设备、数据库、Web系统、服务器、框架、交互协议等。
软件架构师的目标是创建一种系统形态,该形态会以策略为最基本的元素,
并让细节与策略脱离关系,以允许在具体决策过程中推迟或延迟与细节相关的内容。

在开发的早期阶段应该无须选择数据库系统,也不应该选定使用的Web服务,
不应该过早地采用REST模式,也不应该过早地考虑采用微服务框架、SOA框架等,
不应过早地采用依赖注入框架,
软件的高层策略压根不应该跟这些有关。

在开发高层策略时有意地让自己摆脱具体细节的纠缠,就可以将与具体实现相关的细节决策推迟或延后,
因为越到项目的后期,我们就拥有越多的信息来做出合理的决策。

可以让我们有机会做不同的尝试。我们保留这些可选项的时间越长,实验的机会也就越多。
而实验做得越多,我们做决策的时候就能拥有越充足的信息。

一个优秀的软件架构师应该致力于最大化可选项数量。

15.6 设备无关性

第16章 独立性

16.1 用例
系统的架构就必须非常直观地支持应用可能会涉及的所有用例。

16.2 开发
康威定律:任何一个组织在设计系统时,往往都会复制出一个与该组织内沟通结构相同的系统。

16.3 部署
一个设计良好的架构通常不会依赖于成堆的脚本与配置文件,
也不需要用户手动创建一堆“有严格要求”的目录与文件。
总而言之,一个设计良好的软件架构可以让系统在构建完成之后立刻就能部署。
开发一些主组件,让它们将整个系统黏合在一起,正确地启动、连接并监控每个组件。

16.4 保留可选项
一个设计良好的架构应该通过保留可选项的方式,让系统在任何情况下都能方便地做出必要的变更。

16.5 按层解耦
一个系统可以被解耦成若干个水平分层——UI界面、应用独有的业务逻辑、领域普适的业务逻辑、数据库等。

16.6 用例的解耦
用例是系统水平分层的一个个垂直切片。
每个用例都会用到一些UI、特定应用的业务逻辑、应用无关的业务逻辑以及数据库功能。
因此,我们在将系统水平切分成多个分层的同时,也在按用例将其切分成多个垂直切片。
总结出一个模式:
如果我们按照变更原因的不同对系统进行解耦,就可以持续地向系统内添加新的用例,而不会影响旧有的用例。
如果我们同时对支持这些用例的UI和数据库也进行了分组,那么每个用例使用的就是不同面向的UI与数据库,
因此增加新用例就更不太可能会影响旧有的用例了。

16.7 解耦的模式
如果UI和数据库的部分能从业务逻辑分离出来,那么它们就可以运行在不同的服务器上.
这种组件称为“服务”或“微服务”,至于是前者还是后者,往往取决于某些非常模糊的代码行数阈值。
对于这种基于服务来构建的架构,架构师们通常称之为面向服务的架构。

16.8 开发的独立性
只要系统按照其水平分层和用例进行了恰当的解耦,整个系统的架构就可以支持多团队开发,
不管团队组织形式是分功能开发、分组件开发、分层开发,还是按照别的什么变量分工都可以.

16.9 部署的独立性
如果解耦工作做得好,我们甚至可以在系统运行过程中热切换(hot-swap)其各个分层实现和具体用例。

16.10 重复
真正的重复,还只是一种表面性的重复?
一定要小心避免陷入对任何重复都要立即消除的应激反应模式中。
一定要确保这些消除动作只针对那些真正意义上的重复。

16.11 再谈解耦模式
源码层次解耦:控制源代码模块之间的依赖关系,以此来实现一个模块的变更不会导致其他模块也需要变更或重新编译.
部署层次解耦:控制部署单元(如jar文件、DLL、共享库等)之间的依赖关系,以此实现一个模块的变更不会导致其他模块的重新构建和部署。
服务层次解耦:我们可以将组件间的依赖关系降低到数据结构级别,然后仅通过网络数据包来进行通信。
将系统的解耦推行到某种一旦有需要就可以随时转变为服务的程度即可,让整个程序尽量长时间地保持单体结构,以便给未来留下可选项。

一个设计良好的架构应该能允许一个系统从单体结构开始,以单一文件的形式部署,
然后逐渐成长为一组相互独立的可部署单元,甚至是独立的服务或者微服务。
最后还能随着情况的变化,允许系统逐渐回退到单体结构。

一个系统所适用的解耦模式可能会随着时间而变化,优秀的架构师应该能预见这一点,并且做出相应的对策。

第17章 划分边界

过早且不成熟的决策:与系统的业务需求(也就是用例)无关。
这部分决策包括我们要采用的框架、数据库、Web服务器、工具库、依赖注入等。
在一个设计良好的系统架构中,这些细节性的决策都应该是辅助性的,可以被推迟的。
一个设计良好的系统架构不应该依赖于这些细节,而应该尽可能地推迟这些细节性的决策,并致力于将这种推迟所产生的影响降到最低。

将系统分割成组件,其中一部分是系统的核心业务逻辑组件,而另一部分则是与核心业务逻辑无关但负责提供必要功能的插件。
然后通过对源代码的修改,让这些非核心组件依赖于系统的核心业务逻辑组件。
其实,这也是一种对依赖反转原则(DIP)和稳定抽象原则(SAP)的具体应用,依赖箭头应该由底层具体实现细节指向高层抽象的方向。

第18章 边界剖析
第19章 策略与层次

第20章 业务逻辑

业务逻辑是一个软件系统存在的意义,它们属于核心功能,是系统用来赚钱或省钱的那部分代码,是整个系统中的皇冠明珠。
这些业务逻辑应该保持纯净,不要掺杂用户界面或者所使用的数据库相关的东西。
在理想情况下,这部分代表业务逻辑的代码应该是整个系统的核心,其他低层概念的实现应该以插件形式接入系统中。
业务逻辑应该是系统中最独立、复用性最高的代码。

第21章 尖叫的软件架构

21.1 架构设计的主题
软件的系统架构应该为该系统的用例提供支持。
不应该与框架相关,框架只是一个可用的工具和手段,而不是一个架构所规范的内容。

21.2 架构设计的核心目标
一个良好的架构设计应该围绕着用例来展开,这样的架构设计可以在脱离框架、工具以及使用环境的情况下完整地描述用例.
良好的架构设计应该尽可能地允许用户推迟和延后决定采用什么框架、数据库、Web服务以及其他与环境相关的工具。
框架应该是一个可选项,良好的架构设计应该只关注用例,并能将它们与其他的周边因素隔离。

22.2 框架是工具
保持对系统用例的关注,避免让框架主导我们的架构设计。

22.3 可测试的架构设计
通过用例对象来调度业务实体对象,确保所有的测试都不需要依赖框架。

第22章 整洁架构

六边形架构(Hexagonal Architecture)(也称为端口与适配器架构,Ports and Adpaters)
DCI架构
BCE架构

具有同一个设计目标:按照不同关注点对软件进行切割。
也就是说,这些架构都会将软件切割成不同的层,至少有一层是只包含该软件的业务逻辑的,而用户接口、系统接口则属于其他层。

22.1 依赖关系规则
源码中的依赖关系必须只指向同心圆的内层,即由低层机制指向高层策略。

第23章 展示器和谦卑对象

第24章 不完全边界(partial boundary)

构建完整的架构边界是一件很耗费成本的事。
预防性设计在敏捷社区里饱受诟病,因为它违背了YAGNI原则(“You Aren't Going to Need It”,意即“不要预测未来的需要”)。
构建不完全边界的一种方式就是在将系统分割成一系列可以独立编译、独立部署的组件之后,再把它们构建成一个组件。
换句话说,在将系统中所有的接口、用于输入/输出的数据格式等每一件事都设置好之后,仍选择将它们统一编译和部署为一个组件。
显然,这种不完全边界所需要的代码量以及设计的工作量,和设计完整边界时是完全一样的。
但它省去了多组件管理这部分的工作,这就等于省去了版本号管理和发布管理方面的工作。

第25章 层次与边界

第26章 Main组件

在所有的系统中,都至少要有一个组件来负责创建、协调、监督其他组件的运转。我们将其称为Main组件。

第27章 服务:宏观与微观

27.1 面向服务的架构
服务本身并不能完全代表系统架构。
系统中许多其他的函数虽然也起到了隔离行为的效果,但它们显然并不具有架构意义。
服务这种形式说到底不过是一种跨进程/平台边界的函数调用而已。有些服务会具有架构上的意义,有些则没有。

27.2 服务所带来的好处
解耦合的谬论
服务之间的确在变量层面做到了彼此隔离。
然而,它们之间还是可能会因为处理器内的共享资源,或者通过网络共享资源而彼此耦合的。
另外,任何形式的共享数据行为都会导致强耦合。
服务能很好地定义接口,但函数也能做到这一点。
事实上,服务的接口与普通的函数接口相比,并没有比后者更正式、更严谨,也没有更好。

独立开发部署的谬论
大型系统一样可以采用单体模式,或者组件模式来构建,不一定非得服务化。因此服务化并不是构建大型系统的唯一选择。
解耦合谬论已经说明拆分服务并不意味着这些服务可以彼此独立开发、部署和运维。
如果这些服务之间以数据形式或者行为形式相耦合,那么它们的开发、部署和运维也必须彼此协调来进行。

27.3 横跨型变更
系统的架构边界事实上并不落在服务之间,而是穿透所有服务,在服务内部以组件的形式存在.
服务边界并不能代表系统的架构边界,服务内部的组件边界才是。

第28章 测试边界
第29章 整洁的嵌入式架构

第6部分 实现细节
第30章 数据库只是实现细节
第31章  Web是实现细节
第32章 应用程序框架是实现细节


 

  • 11
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值