架构整洁之道-学习笔记
架构”这个词往往使用于“高层级”的讨论中。这类讨论一般都把“底层”的实现细节排除在外。而“设计”一词,往往用来指代具体的系统代码组织结构和实现细节。但是,从一个真正的系统架构师的日常工作来看,这样的区分是根本不成立的。
底层设计细节和高层架构信息是不可分割的。只考虑高层架构,而不考虑设计细节会导致架构师脱离一线,导致架构师永远不了解具体开发代码时会遇到什么问题。而只考虑设计细节而不考虑架构会导致视野的局限性,没有全局观,设计出来的系统可能边界不清楚,组件划分不明确,系统最终成为一团谁也理不清的乱麻。
所以一个好的架构师首先要做高层架构来梳理整个系统的边界,组件划分,从而构建一个职责清晰的架构视图,然后还要对架构进行推演,思考具体实施时会遇到哪些问题。
软件架构的终极目标软件架构的终极目标是,用最小的人力成本来满足构建和维护该系统的需求。
构建:从0到1完成系统开发的成本
维护:新增功能,修改已有功能的成本。(相比构建更重要)
构建从0到1的系统,会很快,不是问题,问题是后期的功能迭代的成本。如果软件架构比较糟糕,那么会导致后期迭代很小的功能也需要花费大量的人力资源。如果在构建系统时通过合理的模块划分,分层,抽象,根据SOLID设计原则来构建软件,则会把系统划分为边界良好的组件,然后提供足够的扩展性,从而会使迭代功能成本比较低
好的设计和架构可以减少系统的构建和维护成本,提高生产力。
行为价值&架构价值行为价值:根据需求实现的用例功能,这些可以给企业带来盈利
架构价值:软件应该容易被修改,新增或者修改已有功能
长远看架构价值更重要。架构混乱会导致系统越来越难维护,后期迭代功能时候成本越来越高。
结构化编程
结构化程序设计则采用自顶向下、逐步求精的设计方法,各个模块通过“顺序、选择、循环”的控制结构进行连接,并且只有一个入口、一个出口。
结构化程序设计的原则可表示为:程序=(算法)+(数据结构)。算法是一个独立的整体,数据结构(包含数据类型与数据)也是一个独立的整体。两者分开设计,以算法(函数或过程)为主。
-
自顶向下
程序设计时,应先考虑总体,后考虑细节;先考虑全局目标,后考虑局部目标。不要一开始就过多追求众多的细节,先从最上层总目标开始设计,逐步使问题具体化。 -
逐步细化
对复杂问题,应设计一些子目标作为过渡,逐步细化。 -
模块化
结构化程序设计
结构化编程范式可将模块递归降解拆分为可推导的单元,这就意味着模块也可以按功能进行降解拆分。这样一来,我们就可以将一个大型问题拆分为一系列高级函数的组合,而这些高级函数各自又可以继续被拆分为一系列低级函数,如此无限递归。更重要的是,每个被拆分出来的函数也都可以用结构化编程范式来书写
goto语句的某些用法会导致某个模块无法被递归拆分成更小的、可证明的单元,这会导致无法采用分解法来将大型问题进一步拆分成更小的、可证明的部分。无限制的goto语句可以直接控制程序执行的随意的转移。而结构化编程限制了goto语句的使用,所以结构化编程对程序控制权的直接转移进行了限制和规范。
面向对象编程
多态其实不过就是函数指针的一种应用。自从20世纪40年代末期冯·诺依曼架构诞生那天起,程序员们就一直在使用函数指针模拟多态了。
但是那时候程序员必须严格按照固定约定来初始化函数指针,并同样严格地按照约定来调用这些指针。只要有一个程序员没有遵守这些约定,整个程序就会产生极其难以跟踪和消除的Bug。面向对象编程语言则为我们消除了人工遵守这些约定的必要,也就等于消除了这方面的危险性。
采用面向对象编程语言让多态实现变得非常简单,让一个传统C程序员可以去做以前不敢想的事情。综上所述,我们认为面向对象编程其实是对程序间接控制权的转移进行了约束。
利用多态可以实现其他领域模块成为业务核心逻辑模块的插件。通过DIP来实现插件的改变不影响业务逻辑模块的编译和部署。
当某个组件的源代码需要修改时,仅仅需要重新部署该组件,不需要更改其他组件,这就是独立部署能力。如果系统中的所有组件都可以独立部署,那它们就可以由不同的团队并行开发,这就是所谓的独立开发能力。
面向对象编程就是以多态为手段使用DIP原则对源代码中的依赖关系进行控制的能力,这种能力让软件架构师可以构建出某种插件式架构,让高层策略性组件与底层实现性组件相分离,底层组件可以被编译成插件,实现独立于高层组件的开发和部署。
函数式编程
一个架构设计良好的应用程序应该将有状态修改的部分和不需要修改状态的部分隔离成单独的组件,然后用合适的机制来保护可变量。软件架构师应该着力于将大部分处理逻辑都归于不可变组件中,可变状态组件的逻辑应该越少越好。
这种数据存储模式中不存在删除和更新的情况,我们的应用程序不是CRUD,而是CR。因为更新和删除这两种操作都不存在了,自然也就不存在并发问题。如果我们有足够大的存储量和处理能力,应用程序就可以用完全不可变的、纯函数式的方式来编程。
所以说,函数式编程对程序中的赋值进行了限制和规范。
设计原则
SOLID原则的主要作用就是告诉我们如何将数据和函数组织成为类,以及如何将这些类链接起来成为程序。即构建组件的原则,组件构建原则则是如何使用组件构建系统。
软件构建中层结构的主要目标如下:使软件可容忍被改动。使软件更容易被理解。构建可在多个软件系统中复用的组件。
正如用好砖也会盖歪楼一样,采用设计良好的中层组件并不能保证系统的整体架构运作良好。正因为如此,我们在讲完SOLID原则之后,还会再继续针对组件的设计原则进行更进一步的讨论,将其推进到高级软件架构部分,即如何使用组件构建系统。
SRP:单一职责原则。
该设计原则是基于康威定律(Conway's Law)[1]的一个推论——一个软件系统的最佳结构高度依赖于开发这个系统的组织的内部结构。这样,每个软件模块都有且只有一个需要被改变的理由。
任何一个软件模块都应该只对某一类行为者负责。而不是简单的只做一件事情。由于一类行为可能有多件事情组成,把相同行为的东西放到一个模块,那么修改他的只有一个原因,就是要改变这类行为的共同内容。
如果A组件不想被B组件上发生的修改所影响,那么就应该让B组件依赖于A组件。
OCP:开闭原则。
该设计原则是由Bertrand Meyer在20世纪80年代大力推广的,其核心要素是:如果软件系统想要更容易被改变,那么其设计就必须允许新增代码来修改系统行为,而非只能靠修改原来的代码。
一个设计良好的计算机系统应该在不需要修改的前提下就可以轻易被扩展。其实这也是我们研究软件架构的根本目的。如果对原始需求的小小延伸就需要对原有的软件系统进行大幅修改,那么这个系统的架构设计显然是失败的
软件架构师可以根据相关函数被修改的原因、修改的方式及修改的时间来对其进行分组隔离,并将这些互相隔离的函数分组整理成组件结构,使得高阶组件不会因低阶组件被修改而受到影响。
OCP是我们进行系统架构设计的主导原则,其主要目标是让系统易于扩展,同时限制其每次被修改所影响的范围。实现方式是通过将系统划分为一系列组件,并且将这些组件间的依赖关系按层次结构进行组织,使得高阶组件不会因低阶组件被修改而受到影响。
LSP:里氏替换原则。
该设计原则是Barbara Liskov在1988年提出的一个著名的子类型定义。简单来说,这项原则的意思是如果想用可替换的组件来构建软件系统,那么这些组件就必须遵守同一个约定,以便让这些组件可以相互替换。
ISP:接口隔离原则。
这项设计原则主要告诫软件设计师应该在设计中避免不必要的依赖。
DIP:依赖反转原则。
该设计原则指出高层策略性的代码不应该依赖实现底层细节的代码,恰恰相反,那些实现底层细节的代码应该依赖高层策略性的代码。如果想要设计一个灵活的系统,在源代码层次的依赖关系中就应该多引用抽象类型,而非具体实现。
软件架构
首先,软件架构师自身需要是程序员,并且必须一直坚持做一线程序员,绝对不要听从那些说应该让软件架构师从代码中解放出来以专心解决高阶问题的伪建议。不是这样的!软件架构师其实应该是能力最强的一群程序员,他们通常会在自身承接编程任务的同时,逐渐引导整个团队向一个能够最大化生产力的系统设计方向前进。也许软件架构师生产的代码量不是最多的,但是他们必须不停地承接编程任务。如果不亲身承受因系统设计而带来的麻烦,就体会不到设计不佳所带来的痛苦,接着就会逐渐迷失正确的设计方向。
软件架构设计的主要目标是支撑软件系统的全生命周期,设计良好的架构可以让系统便于理解、易于修改、方便维护,并且能轻松部署。软件架构的终极目标就是最大化程序员的生产力,同时最小化系统的总运营成本。
基本上,所有的软件系统都可以降解为策略与细节这两种主要元素。策略体现的是软件中所有的业务规则与操作过程,因此它是系统真正的价值所在。而细节则是指那些让操作该系统的人、其他系统以及程序员们与策略进行交互,但是又不会影响到策略本身的行为。它们包括I/O设备、数据库、Web系统、服务器、框架、交互协议等。软件架构师的目标是创建一种系统形态,该形态会以策略为最基本的元素,并让细节与策略脱离关系,以允许在具体决策过程中推迟或延迟与细节相关的内容。
优秀的架构师会小心地将软件的高层策略与其底层实现隔离开,让高层策略与实现细节脱钩,使其策略部分完全不需要关心底层细节,当然也不会对这些细节有任何形式的依赖。另外,优秀的架构师所设计的策略应该允许系统尽可能地推迟与实现细节相关的决策,越晚做决策越好。
软件架构设计本身就是一门划分边界的艺术。边界的作用是将软件分割成各种元素,以便约束边界两侧之间的依赖关系。其中有一些边界是在项目初期——甚至在编写代码之前——就已经划分好,而其他的边界则是后来才划分的。在项目初期划分这些边界的目的是方便我们尽量将一些决策延后进行,并且确保未来这些决策不会对系统的核心业务逻辑产生干扰。正如我们之前所说,架构师们所追求的目标是最大限度地降低构建和维护一个系统所需的人力资源。那么我们就需要了解一个系统最消耗人力资源的是什么?答案是系统中存在的耦合——尤其是那些过早做出的、不成熟的决策所导致的耦合。
为了在软件架构中画边界线,我们需要先将系统分割成组件,其中一部分是系统的核心业务逻辑组件,而另一部分则是与核心业务逻辑无关但负责提供必要功能的插件。然后通过对源代码的修改,让这些非核心组件依赖于系统的核心业务逻辑组件。
软件架构设计的工作重点之一就是,将这些策略彼此分离,然后将它们按照变更的方式进行重新分组。其中变更原因、时间和层次相同的策略应该被分到同一个组件中。反之,变更原因、时间和层次不同的策略则应该分属于不同的组件。
如果我们要将自己的应用程序划分为业务逻辑和插件两部分,就必须更仔细地了解业务逻辑究竟是什么,它到底有几种类型。严格地讲,业务逻辑就是程序中那些真正用于赚钱或省钱的业务逻辑与过程。更严格地讲,无论这些业务逻辑是在计算机上实现的,还是人工执行的,它们在省钱/赚钱上的作用都是一样的。
业务逻辑是一个软件系统存在的意义,它们属于核心功能,是系统用来赚钱或省钱的那部分代码,是整个系统中的皇冠明珠。这些业务逻辑应该保持纯净,不要掺杂用户界面或者所使用的数据库相关的东西。在理想情况下,这部分代表业务逻辑的代码应该是整个系统的核心,其他低层概念的实现应该以插件形式接入系统中。业务逻辑应该是系统中最独立、复用性最高的代码。
一个良好的架构设计应该围绕着用例来展开,这样的架构设计可以在脱离框架、工具以及使用环境的情况下完整地描述用例。这就好像一个住宅建筑设计的首要目标应该是满足住宅的使用需求,而不是确保一定要用砖来构建这个房子。架构师应该花费很多精力来确保该架构的设计在满足用例需要的情况下,尽可能地允许用户能自由地选择建筑材料(砖头、石料或者木材)。
整洁架构分层架构,端口适配器架构,虽然这些架构在细节上各有不同,但总体来说是非常相似的。它们都具有同一个设计目标:按照不同关注点对软件进行切割。也就是说,这些架构都会将软件切割成不同的层,至少有一层是只包含该软件的业务逻辑的,而用户接口、系统接口则属于其他层。按照这些架构设计出来的系统,通常都具有以下特点。
-
独立于框架:这些系统的架构并不依赖某个功能丰富的框架之中的某个函数。框架可以被当成工具来使用,但不需要让系统来适应框架。
-
可被测试:这些系统的业务逻辑可以脱离UI、数据库、Web服务以及其他的外部元素来进行测试。- - 独立于UI:这些系统的UI变更起来很容易,不需要修改其他的系统部分。例如,我们可以在不修改业务逻辑的前提下将一个系统的UI由Web界面替换成命令行界面。
-
独立于数据库:我们可以轻易将这些系统使用的Oracle、SQL Server替换成Mongo、BigTable、CouchDB之类的数据库。因为业务逻辑与数据库之间已经完成了解耦。
-
独立于任何外部机构:这些系统的业务逻辑并不需要知道任何其他外部接口的存在。
我们要通过下图将上述所有架构的设计理念综合成为一个独立的理念
首先整洁结构也是分层的,但是他强调底层逻辑要源码依赖高层逻辑,高层是核心业务逻辑,其他底层可以作为插件的方式插入进来。
通过将系统划分层次,并确保这些层次遵守依赖关系规则,就可以构建出一个天生可测试的系统,这其中的好处是不言而喻的。而且,当系统外层的这些数据库或Web框架过时的时候,我们还可以很轻松地替换它们
Main组件Main组件是系统中最细节化的部分——也就是底层的策略,它是整个系统的初始点。在整个系统中,除了操作系统不会再有其他组件依赖于它了。Main组件的任务是创建所有的工厂类、策略类以及其他的全局设施,并最终将系统的控制权转交给最高抽象层的代码来处理。Main组件中的依赖关系通常应该由依赖注入框架来注入。在该框架将依赖关系注入到Main组件之后,Main组件就应该可以在不依赖于该框架的情况下自行分配这些依赖关系了。
实现细节数据库、Web、应用程序框架都是实现细节。