软件架构整洁之道-读书笔记(3)

第五部分:软件架构

第十五章:什么是软件架构

1、架构师是什么样的人?

首先软件架构师必须是能力最强的一群程序员,他们的代码产量可能不是最多的,但是他们必须不停的承接编程任务。如果不亲自承受因系统设计而带来的麻烦,就体会不到设计不加所带来的的痛苦,接着就会逐渐迷失正确的设计方向。

软件架构这项工作的实质就是规划如何将系统划分成组件,并安排好组件之间的排列关系,以及组件之间互相通信的方式。

2、架构设计和系统行为的关系

软件架构的直接目的不是保证系统能否正常工作,毕竟世界上也有很多架构设计糟糕但是工作正常的软件系统。当然,架构对”软件正常工作“也有重要影响,但更多的时候,这种影响是被动的、修饰性的,而不是主动的、必不可少的。架构设计中,能直接影响系统行为的可选项少之又少。

3、软件架构设计的目标

软件架构的主要目标是支撑系统的全生命周期,让系统便于理解、易于修改、方便维护,并且轻松部署。软件架构的终极目标是最大化程序员的生产力,同时最小化系统的总运营成本。

软件架构着眼于软件系统的生命周期,而非当前系统的功能。

4、架构与开发

一个开发起来困难的软件系统是不会有一个长久、健康的生命周期的,所以系统架构作用要方面其开发团队的开发工作。一个只有5个开发人员的小团队,完全可以非常高效地开发一个没有明确定义组件和接口的单体系统。这样的团队可能会发现软件架构在早期反而是一种障碍,这就是许多系统都没有一个设计良好的架构的原因。

另一方面,如果一个软件系统由5个不同的团队合作开发,不将系统划分成定义清晰的组件和可靠稳定的接口,开发工作就没法继续推进。如果忽略其他因素,该系统的架构会逐渐演变成五个组件,一个组件对应一个团队。不过注意:这种架构不太可能是该系统在部署、运行、维护方面的最优方案。

随着软件行业的发展,各种技术框架的成熟,单个程序员的产量越来越高,小团队也需要很好的架构设计。

5、架构与部署

软件系统必须部署才可用,一个系统的部署成本越高,可用性就越低。因此,实现一键式的轻松部署应该是我们设计软件架构的一个目标。

但是很不幸,在系统的早期我们很少考虑部署方面的事情,这常常导致一个易于开发、难于部署的系统。比如”微服务架构“,它的组件边界清晰、接口稳定、非常利于开发。当时当我们实际部署这种系统时,就会发现其微服务的数量已经令人望而生畏,而配置这些微服务之间的连接关系、启动顺序都会成为系统出错的主要来源。

如果架构师能够预先考虑到这些问题,可能就会有意地减少服务数量,采用进程内部组件和外部服务混合的架构,以及更加集成式的链接管理方式。

对于小团队来说,一定要严控服务数量。本人曾经将原本独立的服务降级为普通的maven组件,用简单的socket通信取代RabbitMq。

6、架构与运行

软件架构对系统运行的影响远不及它对开发、部署、维护的影响,因为几乎所有的运行问题都可以通过增加硬件的方式来解决,从而避免软件架构的重新设计。

并不是说,我们不应该为了让系统更好地运转而优化架构设计,而是从投入产出比的角度考虑,我们的优化重心应该偏向其他几个方面。或者可以换一种说法,良好的软件架构让程序员对系统的运行过程一目了然,很少出现运行问题,即使有问题,也很容易局部化。

7、架构与维护

在软件系统的所有方面,维护所需的成本是最高的。满足永不停歇的新功能需求,以及修改层出不穷的BUG,将会占去绝大部分人力资源。

系统维护的主要成本来自“探秘”和“风险”这两件事。“探秘”指我们对现有系统的挖掘,确定新增功能或被修复问题的最佳位置和最佳方式。而“风险”则指当我们进行上述修改时,总有衍生出新问题的可能。

“探秘”一词用得绝佳,形象地体现了开发者和系统之间的关系。

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

8、架构尽量保持可选项

软件被发明出来就是因为饿哦们需要一种灵活、便捷的方式来控制机器的行为。架构让软件维持”软“性的方法就是:尽可能长时间地保留尽可能多的可选项

那么到底哪些选项是我们应该保留的?就是哪些无关紧要的细节。基本上,所有的软件系统都可以降解为策略与细节这两种主要元素,策略体现的是软件中所有的业务规则和操作过程,它是软件真正的独特价值所在。而细节则是指那些让使用者、其他系统、程序员与策略进行交互,但是又不会影响到策略本身的机制。它们包括了IO设备、数据库、Web界面、服务器硬件、框架、交互协议等等。

软件架构师的目标是创建一种系统形态,该形态以策略为核心元素,并让细节与策略脱钩,允许在决策过程中推迟或延迟细节相关的内容。

在开发的早期,应该无须选择数据库系统,因为高层策略不应该关心使用哪种数据库,关系型、分布式、多级,或仅仅是一些文本文件。不应该过早地采用REST模式,也不应该过早地考虑才采用微服务框架。不应过早地采用依赖注入框架,因为高层策略不应该操心如何解析系统依赖。

我们保留这些可选项的时间越长,实验的机会也就越多,最终决策的时候就能拥有越充足的信息。

保留可选项并非没有代价,可以采用折中的方案:增加适配层以隔离高层策略与技术细节。另一方面,那些优秀的技术框架显然也考虑到了这一点,越来越注重轻量级和非侵入性,降低使用者这方面的担忧。

第十六章:独立性(解耦)

1、按层解耦

从用例的角度看,架构师的目标是让系统结构支持所有的用例,但问题恰恰是我们无法预知全部的用例。好在架构师还是知道整个系统的设计意图的,也就是说,知道自己要设计的是一个购物车系统,或是运输清单系统,所以架构师可以通过采用SRP或CCP原则,以及既定的设计意图来隔离那些变更原因不同的部分,集成变更原因相同的部分。

哪些部分的变更原因是不同的呢?有些情况是显然易见的。譬如,用户界面的变更肯定和业务逻辑不相关,而业务用例则通常在两边都有相关元素。

而业务逻辑既可以是与应用程序紧密相关的,也可以是更具有普适性的。例如,对输入字段的校验是一个应用程序本身的特有逻辑;而计算账户利息和清点库存则是与具体领域更为先关的业务逻辑。这两种不同的业务逻辑通常有换个不同的变更速率和变更原因。

此即“应用逻辑”和“领域模型”之分。

数据库以及所采取的查询语言,表结构,都是系统的技术细节,它们与业务规则或UI毫无关系。它们的变更原因可能是硬件设施、吞吐量需求…,也应当与其他部分隔离。

这样一来,我们可以得到一个具备一定普适性的分层结构:UI界面,应用独有的业务逻辑,领域普适业务逻辑,数据库

2、用例解耦

还有什么不同原因的变更呢?答案正是用例本身!譬如说,添加新订单的用例和删除订单的用例在发生变更的原因上肯定不同。因此我们按照用例来切分系统是非常自然的选择。

这种切分,是上述系统水平分层的一个个垂直切片,每个用例都会用到一些UI界面,应用独有的业务逻辑,领域普世业务逻辑,数据库。

3、解耦之于运行

现在可以想想这些解耦模式对系统运行的意义。如果不同用例得到了良好的隔离,那么需要高吞吐量的用例就和需要低吞吐量的用例互相自然分开了,如果UI和数据库的部分能从业务逻辑分离出来,那么就可以运行在不同的服务器上,而且需要较大带宽的应用也可以在运行更多的实例。

从系统运行的角度出发,我们的解耦动作还需要注意选择恰当的模式。譬如,为了在不同的服务器上运行,被隔离的组件不能依赖于某个处理器上的同一个地址空间,它们必须是独立的服务,然后通过网络来进行通信。

许多架构师将上面的这种组件成为“服务”或“微服务”。对于这种基于服务来构建的架构,通常被称之为面向服务的架构(SOA)。

注意:这里并没有鼓吹SOA是最佳架构。

4、解耦促进开发独立性

当系统按照水平分层和用例进行恰当的解耦之后,就可以支持多团队开发,不管团队组织形式是分功能、分组件、分层开发,还是按照别的什么变量分工。

5、解耦促进部署独立性

如果解耦工作做得好,我们可以在系统运行过程中热切换各个分层和具体用例。我们增加、修改用例只需要在系统中添加或启动一些服务即可,其他部分完全不受影响。

6、重复

架构师们会经常钻进一个牛角尖——害怕重复。

但是重复也存在很多种情况,有些是真正的重复,每个实例上发生的变更必须同时应用到所有的副本上。也有些是假的重复,或者说是表面性质的。如果有两段看起来重复的代码,它们走的是不同的演进路径,也就是说它们有不同的变更速率和变更缘由,那么这两段代码就不是真正的重复。勉强将这些表面上的重复统一起来,是费力不讨好的工作。

就好像大街上碰到两个长得有点像的人,他们之间能有啥关系呢?

7、解耦模式

一个系统有很多种解耦方式:

  • 源码层次

控制好模块之间在源码层次的依赖,并通过函数调用来交互,这些模块运行在运行在同一个进程下面,此即所谓单体结构;

  • 部署层次

将系统划分为可动态部署的单元(譬如jar,DLL),仍然在同一个进程下运行,但可以实现一个模块的变更不会导致其他模块的重新构建和部署。

maven依赖机制是这种模式的典型

  • 服务层次

将系统划分为服务,各自作为独立的进程运行,将组件之间的依赖关系降低到通信协议级别。

在项目的早期,很难知道那种模式最好,而且随着项目逐渐成熟,最好的模式可能会发生变化。一种当前流行的解决方案是,默认就采用服务层次的解耦,这种做法的问题在于它的成本很高,而且变相鼓励粗粒度的解耦(毕竟,无论微服务有多“微”,其解耦的精细度都是不够的)。

通常,我倾向于将系统的解耦推行到某种一旦有需要就可以随时转变为服务的程度,让整个程序尽量长的时间保持单体结构。一个设计良好的架构,应该允许一个系统从单体结构开始,然后逐渐成长为一组相互独立的可部署单元,甚至独立的服务。最后还能随情况的变化,允许系统逐渐退回到单体结构。

本人认为,应该混合使用三种解耦模式。

第十七章:划分边界

软件架构设计本身就是一门划分边界的艺术。边界的作用是将软件分割成各种元素,以便约束边界两侧之间的依赖关系。

有些边界是在项目初期就划分好,而其他边界则是后来才划分的。在项目初期划分这些边界的目的是方便我们尽量将一些决策延后进行,并确保这些决策不会对系统的核心业务逻辑产生干扰。

正如之前所说,架构师所追求的目标是最大限度地降低构建和维护一个系统所需的人力资源,那么我们就需要了解一个系统组最消耗(或者是最浪费)人力资源的是什么?答案是系统中存在的一切耦合——尤其是那些过早做出的、不成熟的决策锁导致的耦合。

那么,哪些决策会被认为是过早且不成熟的呢?答案是那些与系统的业务需求无关的决策。比如采用的框架、数据库、web服务器、工具库、依赖注入等。

这里必须与日俱进地说明一下,随着那些优秀(集成简单、侵入性低)的工具出现,这方面的决策成本已经大大降低。

插件式架构

事实上,软件开发技术发展的历史,就是一个如何想方设法方便地增加插件,从而构建一个可扩展、可维护的系统架构的故事。系统的核心业务逻辑必须和其他组件隔离,保持独立,这些其他组件要么是可以去掉的,要么可以有多种实现。

这里的插件,强调的是独立性、可选性和可替换性,不是指技术层面的可热插拔性。

因此,GUI在架构设计中可以插件的形式存在。由于GUI能够直观感受到,有些人以GUI视角来定义整个系统,这是大错特错。对于核心业务逻辑来说,GUI(包括IO)是无关紧要的细节。我们可以提供一个Web界面,或者SOA接口,甚至命令行来承担输入输出。

数据库也是类似的,它可以被替换成不同类型的SQL数据库、NoSQL数据库,甚至基于文件系统的数据库。

虽然这些替换工作可能并不轻松,但插件式架构视角至少为我们提供了这种可能性。

第十八章:边界剖析

一个系统架构是由一系列软件组件以及它们之间的边界共同定义的,而这些边界有不同的存在形式。在运行时,跨边界调用是指边界一侧的函数调用另一侧的函数,并同时传递数据的行为。

单体结构

单体结构内部的架构边界没有一个具体的物理形式,它们只是对同一个进程、同一个地址空间内的函数和数据进行了某种划分。虽然这类系统的架构边界在部署的过程中不可见,但并不意味着它们就不存在或没有意义。

这类架构一般都需要利用某种动态形式的多态来管理其内部的依赖关系,这也是为什么面向对象编程近几十年来逐渐成为一种重要的编程范式的原因之一(否则架构师就只能使用函数指针来解耦组件,由于函数指针过于危险,于是在权衡利弊之后,就干脆放弃划分组件了)。

单体结构中,组件之间的交互就是普通的函数调用,迅速而廉价。由于交互是源码层次的,一定程度上,需要开发者自律地维护组件边界。

部署层次

常见的物理形式有DLL,jar,RubyGem,以及Unix共享库等。这类组件在部署时不需要重新编译,因为它们是以二进制形式或其他等价的可部署形式交付的。除此之外,几乎与单体结构是一样的。

线程

线程既不属于架构边界,也不是部署单元,它仅仅是一种管理并调度程序执行的方式。一个线程既可以被包含在单一组件中,也可以横跨多个组件。

本地进程

这种系统架构的隔离程度介于单体和服务之间,进程之间通过socket来实现通信,当然也可以通过特定操作系统提供的方式来提高通信效率。该系统架构的目标是让底层进程成为高层进程的一个插件。

本地进程之间的跨边界通信需要用到系统调用、数据编解码、进程上下文切换,成本相对会高一些。

服务

系统架构中最强的边界形式就是服务,一个服务就是一个进程,但不依赖具体的运行位置。两个互相通信的服务可以处于单一物理机器上,也可以彼此位于不同的机器上。服务会始终假设它们之间的通信将全部通过网络进行。

服务之间的跨边界通信,相对函数调用来说,速度是非常缓慢的,其往返时间可以从几十毫秒到几秒不等。因此我们在划分架构边界时,一定要尽可能地控制通信次数,而且必须要能够适应高延时。

我们可以在服务层次上使用与本地进程类似的策略,让较低层次的服务成为较高层次服务的插件。为此,我们要确保高层服务的源码中没有任何与底层服务相关的物理信息(比如URI)。

第十九章:策略与层次

本质上,所有的软件系统都是一组策略语句的集合;可以说计算机程序不过就是一组仔细描述如何将输入转化为输出的策略语句的集合。在大多数系统中,整体业务策略通常可以被拆解为多组更小的策略:一部分策略语句专门用户描述计算部分的业务逻辑,另一部分策略语句则负责描述计算报告的输出格式。

软件架构设计的工作重点之一就是,将这些策略彼此分离,然后按照变更的方式进行重新分组。其中变更原因、时间和层次相同的策略应该被分到同一个组件中。

层次(Level)

我们对“层次”的定义严格按照”离输入&输出之间的距离“来定义的。也就是说,一组策略具体系统的输入/输出越远,它所属的层次越高;直接管理输入/输出的策略在系统中的层次是最低的。

以加密程序为例,可简单分为三个模块:读入数据模块、加密计算耐模块、输出加密后数据模块;显然,加密计算模块位于最上层。

高层策略一般更变没有那么频繁,即使发生变更,其原因也比底层策略所在的组件更重大。反之低层策略则很有可能会频繁地进行一些小的变更。一个加密程序,它的加密算法一旦调试完毕,几乎没有变更的可能(如果要变更加密算法,那就是另一个加密程序了),但是输入/输出模块则随着加密程序被移植到不同运行环境而发生变更。

由于变更频率的不同,理所当然地,高层策略的代码不应该依赖低层策略的代码,应该反过来才行。加密计算模块应该定义好输入&输出策略的接口,低层模块遵循该接口定义并实现从不同的设备读取&写入数据。从另一个角度来说,低层组件应该成为高层组件的插件。

系统不同组件之间,数据&调用的流向,与代码的依赖方向,有可能是相反的,此即DIP(依赖反转)原则。

第二十章:业务逻辑

如果我们要将自己的应用程序分为业务逻辑和插件两部分,就必须仔细地了解业务逻辑究竟是什么,它到底有几种类型。

严格上来讲,业务逻辑就是程序中那些真正用来赚钱或省钱的逻辑与过程。更严格地讲,无论这些业务逻辑是在计算机上实现的,还是人工执行的,他们在省钱&赚钱上的作用都是一样的。

我们通常称这些逻辑为“关键业务逻辑”,因为它们是一项业务的关键部分,无论是否有自动化的系统来执行这项业务。关键业务逻辑通常需要处理一些数据,这些数据称为“关键业务数据”,还是因为这些数据无论自动化程序存在与否,都必须要存在。

业务实体

关键业务逻辑和关键业务数据是紧密相关的,他们很适合放在同一个对象中处理,我们称这种对象为“业务实体”。

业务实体要么直接包含关键业务数据,要么可以很容易地访问这些数据。业务实体的接口层则是哪些实现关键业务逻辑、操作关键业务数据的方法组成的。

业务实体不一定非要用面向对象语言的类来实现,业务实体这个概念只要求我们将关键业务数据和关键业务逻辑绑定在一个独立的软件模块内。

很明显,”业务实体“与领域建模方法中”业务实体”概念不太一样,本人认为,这里的“业务实体”基本涵盖了整个领域模型。

用例

并不是所有的业务逻辑都是一个纯粹的业务实体,有些业务路基通过定义或限制自动化系统的运行方式来实现业务。这些业务逻辑不能靠人工来执行,它们只有作为自动化系统的一部分才有意义。

比如,一个银行借贷应用程序:首先客户必须能够通过屏幕填写所有的联系信息,并通过相关验证;其次客户只有在信用值大于既定阈值时才能进入还款预估页。

上面所描述的就是一个“用例”,用例本质上就是关于如何操作一个自动化系统的描述,它定义了用户需要提供的输入数据、用户应该得到的输出信息,以及产生输出所应该采取的处理步骤。

用例所描述的是某项特定应用情景下的业务逻辑,它并非业务实体中所包含的关键业务逻辑。

用例描述

用例非正式地描述了数据输入&输出接口以外,并不详细描述用户界面。也就是说,如果我们只看用例,是没有办法分辨出系统是在Web平台上交付的,还是通过某种富客户端;或者是命令行模式交付,还是以一个内部服务的模式交付的。

这一点非常重要,用例并不描述系统与用户之间的接口,它只描述该应用在特定情境下的业务逻辑。这些业务逻辑所规范的是用户与业务实体之间的交互方式,它与数据输入&输出系统的方式无关。

在系统中,用例本身也是一个对象,该对象包含了一个或多个实现了特定应用情景的业务逻辑函数。除此之外,用例对象中也包含了输入数据&输出数据以及相关业务实体的引用,以方面调用

业务实体并不知道哪个业务用例在控制它们,这是依赖反转原则的另一个场景:业务实体是高层概念,用例相对而言是低层概念。

请求响应模型

通常情况下,用例会接受输入数据,并产生输出数据。但是在一个设计良好的架构中,用例对象通常不应该知道数据展现给用户或其他组件的方式,很显然,我们不希望这些用例代码中出现HTML和SQL。

用例所接受的输入应该是一个简单的请求性数据结构,而返回的应该是一个简单的响应性数据结构,这些数据结构中不应该存在任何依赖关系。它们并不派生自HttpRequest和HttpResponse这样的框架接口,它们应该与Web无关,也不应该涉及任何用户界面的细节。

这种独立性非常关键,如果这里的请求和响应模型不是完全独立的,它们用到这些模型的用例就会依赖这些模型所带来的的各种依赖关系。

有些人会选择直接在请求响应模型中直接使用业务实体对象,但请一定不要这样做。这两种对象的存在的意义是非常、非常不一样的,随着时间的推移,这两个对象会以非常不同的原因、不同的速率发生变更。所以将他们以任何形式整合在一起都是对CCP(共同闭包原则)和SRP(单一职责原则)的违反。这样做的结果,往往会导致代码中出现了很多分支判断语句和中间数据。

第21章:尖叫的软件架构

假设我们现在在看某个建筑的设计架构图,那么我们能看到什么呢?如果这是一个住宅建筑架构图,那么我们很有可能看到一个大门,然后是连接起居室的通道,同时还可能看到一个餐厅,一个厨房;当我们阅读这个架构图时,应该不会怀疑这是一个单户住宅,几乎整个架构设计都在尖叫着告诉你:这是一个”家“。

那么我们的软件系统架构设计应该”喊“些什么呢?当我们查看它的顶层结构目录,以及顶层软件包中的源码时,他们究竟是在喊”健康管理系统“、”账户系统“、”库存管理系统“,还是在喊:”Redis“、“Spring”、“HTML”这样的技术名字呢?

一个系统框架应该着重展示系统本省的设计,而并非该系统所使用的框架。如果我们要构建的是一个医疗系统,新来的程序员第一次看到源代码的时候,就应该知道这是一个医疗系统。

架构的目标是支持用例

Ivar Jacobson关于架构设计的那本书,提出了一个观点:软件的系统架构应该为该系统的用例提供支持。就像住宅建筑规划图凸显建筑用途一样,软件架构设计图非常明确地凸显该应用程序会有哪些用例。

架构设计不应该是与框架相关的,对于我们来说,框架只是一个可用的工具和手段,不是一个架构所规范的内容。

框架是工具而不是生活信条

框架通常可以是非常强大的、非常有用的。但框架作者往往对自己写出的框架有极深的信念,他们所写出的使用手册都是从如何成为该框架的虔诚信徒的角度来描绘如何使用这个框架的。甚至于,框架教程也会出现这种传教士模式。他们会告诉你,某个框架是能够包揽一切、超越一切、解决一切问题的存在。

但这不应该是你的观点,我们一定要带着怀疑的态度来审视每一个框架,权衡使用一个框架的利弊。无论如何,我们需要仔细地考虑如何能保持对系统用例的关注,避免让框架主导我们的架构设计。

第22章:整洁架构

过去几十年中,我们曾见证过一系列关于系统架构的想法,列举如下:

  • 六边形架构
  • DCI架构
  • BCE架构

虽然这些架构在细节上各有不同,它们都具有同一个设计目标:按照不同关注点对软件进行切割。这些架构都会将软件切割成不同的层,至少有一层只包含该软件的业务逻辑的,而用户接口、系统接口则属于其他层。按照这些架构设计出来的系统,通常都具有以下特点:

  • 独立于框架:不依赖某个功能丰富的框架,框架可以被当做工具来使用,但不需要让系统来适应框架;
  • 可被测试:这些系统的业务逻辑可以脱离UI、数据库、Web服务以及其他的外部元素来进行测试;
  • 独立于UI:这些系统的UI变更起来很容易,不需要修改其他系统部分;
  • 独立于数据库:我们可以轻易地将这些系统使用的数据库换成其他持久化技术;
  • 独立于任何外部机构:这些系统的业务逻辑并不需要知道任何其他外部接口的存在。

上面所有架构的设计理念综合成为一个独立的理念图:

软件架构整洁之道-读书笔记(3)_第1张图片

层次关系

同心圆分别代表软件系统中不同层次,越靠近中心,其所在的软件层次就越高。基本上,外层圆代表的是机制,内层圆代表的是策略。源码层的依赖关系必须只指向同心圆的内存,即由低层机制指向高层策略。

同样的道理,外层圆中使用的数据格式也不应该被内层圆中的代码所使用,尤其是当数据格式是由外层圆的框架所生成的时候。

只有(必须有)四层吗?

上图的同心圆知识为了说明架构的结构,没有某个规定约定一个系统的架构有且只有四层。然而,这其中的依赖关系原则是不变的,层次越靠内其抽象和策略的层次就越高,最外层的圆包含的是最具体的实现细节。

个人看法:绝大多数情况下,四层的架构足以;层次太多会适得其反。如果是简单系统,反而可以简化一下,比如去掉用例层,将它并入控制器部分。又或者,对于面向富客户端的系统,不需要单独的用户界面,有展示器就足矣。

跨越边界

上图的右下侧,示范了架构中跨边界的情况:控制器、展示器与下一层用例之间的通信过程。这里控制流的方向是,从控制器开始,穿过用例,最后执行展示器的代码;但同时,源码中的依赖方向是由外指向内的。这里我们通常需要依赖反转原则来解决这种相反性。

个人看法:控制器调用用例,用例调用业务实体,通过方法返回值传递结果数据,控制器再将结果数据传递给展示器不就行了吗?不过这样一来,控制器会依赖展示器的代码,违背了依赖严格向内的原则,放宽到允许同层次模块之间的依赖。

哪些数据会跨越边界

一般来说,会跨越边界的数据在数据结构上都是很简单的,如果可以的话,我们会尽量采用一些基本的结构体或简单的可传输数据对象。不要投机取巧地直接传递业务实体或者数据库记录对象。同时这些传递的数据结构中,也不应该存在违反依赖规则的依赖关系。

实践中,本人没有严格执行这一条,放宽了限制:当内层数据结构(业务实体和数据库记录对象)完全满足外层需求时,可以传递,但不允许为了外层需求而修改内存数据结构。

第23章:展示器和谦卑对象

22章的简洁架构图引入了展示器的概念,后者实际上是谦卑对象(humble object)模式的一种形式,这种设计模式可以很好地帮助我们识别和保护系统架构的边界。

谦卑对象模式

这种模式最初的设计目的,是帮助单元测试的编写者区分容易测试的行为和难以测试的行为,并将它们隔离。其设计思想非常简单,将两类行为拆分成两组模块或类,其中一组模块被称为谦卑组,包含了系统中所有难以测试的行为,而这些行为已经被简化到不能再简化了。另一组模块则包含了所有不属于谦卑对象的行为。

比如,GUI通常难以进行单元测试的,因为让计算机自行检查屏幕内容是非常难的事情,然而GUI中大部分行为实际上是很容易测试的,这时候我们就可以利用谦卑对象模式将GUI这两种行为分拆分为展示器和视图两部分。

展示器与视图

视图部分术语谦卑对象,这种对象的代码通常应该越简单越好,它只负责将数据填充到GUI上,而不应该对数据进行任何处理。展示器则是可以测试的对象,它负责从应用程序中接受数据,然后按视图的需要将这些数据格式化,组成视图模型传递给视图部分。

总而言之,应用程序所能控制的、要在屏幕上显示的一切东西,都应该在视图模型中以字符串、布尔值或枚举值的形式存在。然后视图部分除了加载视图模型所需要的值,不应该做热呢和其他事情。此时我们才能说视图是谦卑对象。

展示器输出的视图模型本质上也是谦卑对象,不应该包含任何逻辑。

本人经验:能放在后端的逻辑就不要放到前端(让整个前端越谦卑越好)

数据库网关和数据映射器

数据库网关是用例交互器与数据库中间的组件,它本质上是一个多态接口,包含了应用程序在数据库上所要执行的创建、读取、更新、删除等所有操作。

数据映射器(ORM)归属数据库层,本质上是一种实现数据库网关接口的机制,它在数据库和数据库网关接口之间个屁口岸;另一种谦卑对象的边界。

因此,数据库记录对象,也应该当做谦卑对象看待。

服务监听器

系统外部服务边界处也应该运用谦卑对象模式吗?答案是肯定的。我们的应用程序会将数据加载到简单的数据结构中,并将这些数据结构跨边界传输给能够将其格式化并传递到其他外部服务的模块。在输入端,服务监听器会负责从服务接口中接受数据,并将其格式化成该应用程序易用的格式。

总而言之,上述数据结构(谦卑对象)可以进行跨服务边界传输。

小结

每个系统架构的边界处,都有可能发现谦卑对象模式的存在。因为跨边界的通信肯定需要用到某种简单的数据结构,而边界则会自然而然地将系统分割成难以测试的部分与容易测试的部分。所以,在系统的边界处运用谦卑对象模式,我们可以大幅地提高整个系统的可测试性。

第24章:不完全边界

构建完整的架构边界是一件很耗费成本的事:设计双向的多态边界接口,输入输出数据结构,依赖关系管理…,这里会设计大量的前期工作以及后期维护工作。很多情况下,优秀的架构师都认为设计架构边界的成本太高了——但为了应对将来可能的需要,通常还是希望预留一个边界。

这种预防性设计在敏捷社区饱受诟病,因为它违背了YAGNI原则(You Aren’t Going to Need It)。然而,架构师本省的工作不就是要做这样的预见性设计吗?这时候,我们就需要引入“不完全边界”。

构建不完全边界有很多方式,通过简单的边界(本地服务接口、策略模式、外观模式等)将规范组件之间的依赖关系,最终所有的组件还是统一编译和部署为一个组件。

第25章节:层次与边界

这一章的题目不知所云,内容上是上一章的延续。

架构边界可以存在任何地方,作为架构师,我们必须要小心审视究竟在什么地方才需要设计架构边界,另外还需要弄清楚完全实现这些边界将会带来多大成本。

同时,我们也必须了解,如果先忽略这些边界,后续再添加会有多困难。

添加边界成本是一个重要的权衡因素,如果这个成本较低,那么在情况不明的时候,不添加为好;因为一旦添加,还需要持续地承担维护成本。

这不是一个一次性的决定,我们不能在项目开始的时候就决定所有边界。必须持续观察系统的演进,时刻注意哪里可能需要设计边界。

第26章:Main组件

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

Main组件是系统中的最细节化的部分,也就是底层的策略,它是整个系统的初始点。Main组件的任务是创建所有的工厂类、策略类以及其他的全局设施,并最终将系统的控制权转交给最高抽象层的代码来处理。

Main组件也可被视为应用程序的插件——这个插件负责设置起始状态、配置信息、加载资源。我们可以为一个系统设计多个Main组件,让它们各自对应于不同的配置。

第27章:宏观与微观

只要使用了服务,就等于有了一套架构?

这是明显错误的思想。如前文所述,架构设计的任务是找出高层策略和底层策略细节之间的架构边界,同时保持这些边界遵守依赖关系规则。所谓服务,只是一种比函数调用方式成本稍高的,分割应用程序行为形式之一,与系统架构无关。

当然,有些服务并不具备系统架构上的意义,比如,用服务来隔离不同平台或进程中的程序行为这件事本身就很重要。

服务能够解耦?

很多人认为将系统拆分成服务的一个最重要的好处就是让每个服务之间实现强解耦。毕竟,每个服务都是以一个不同的进程来运行的,因此服务不能访问彼此的变量;其外,服务之间的接口一定是充分定义的。

在一定程度上,上面的说法是对的。然而,它们之间还是可能会因为处理器内的共享资源,或者通过网络共享资源彼此耦合(比如一个服务需要另一个服务提供的资源才能运行)。另外,任何形式的共享数据都会导致耦合:如果给服务之间传递的数据结构增加一个新字段,那么每个需要操作操作这个字段的服务都必须要做出响应的变化,这些服务是间接彼此耦合的。

再说服务能够很好定义接口的事,函数也能做到这一点。事实上,服务的接口与普通的函数接口相比,并没有后者更正式、更严谨,也没有更好。

独立开发部署谬论

人们认为的另一个使用服务的好处是,不同的服务可以由不同的专门团队负责和运维。这种观点认为大型系统可以由几十个、几百个、甚至几千个独立开发部署的服务组成。

这样的理念有一些道理,也仅此而已。无数的事实证明,大型系统一样可以采用单体模式,或者组件模式来构建,不一定非得服务化。如果不能做到完全解耦,这些服务的开发、部署和运维也必须彼此协调来做。

小结

架构的系统边界并不落在服务之间,而是穿透所有的服务,在服务内部以组件形式存在。一个服务可能是一个独立组件,也可能由几个组件组成,其中的组件以架构边界的形式互相隔离。

个人认为,服务的划分更多是从运行角度来考虑的:不同的系统功能的稳定性、性能需求、运行环境差异,导致我们将相关组件打包成一个服务。

第28章:测试边界

测试是一种系统组件

测试应该是系统的一部分吗?还是应该独立于系统之外存在的呢?单元测试和集成测试是不同的东西吗?

我们没有卷入这些问题的辩论的必要,因为从架构的角度来讲,所有的测试都是一样的。测试也是一种系统组件,也要遵守依赖关系原则的,因为其中总是充满了各种细节信息,非常具体,所以它始终都是向内依赖于被测试部分的。

事实上,我们可以将测试组件视为系统架构中最外圈的程序,它们始终是向内依赖的,而且系统中没有其他组件依赖于它们。

测试组件可以独立部署

大部分测试组件都是被部署在测试环境,而不是产生环境中。所以即使在那些不需要独立部署的系统中,其测试代码也总是独立部署的。

可测试性设计

由于测试代码的独立性,以及往往不会被部署到生产环境,开发者通常会在系统设计过程中,忽视测试的重要性,这种做法是极为错误的。测试如果没有被集成到系统设计中,往往非常脆弱。

关键之处在于耦合,如果测试代码与系统是强耦合的,他就随着系统变更而变更。哪怕只是系统中组件的一点小变化,都可能会导致许多与之相耦合的测试出现问题。

结构性耦合是测试代码所具有的耦合关系中最强大、最阴险的一种新式。假设我们有一组测试,它针对每个产品类都有一个对应的测试类,每个产品函数都有一个对应的测试函数。显然,该测试与应用程序在结构上是紧耦合的。

测试专用API

实现可测性设计的方法注意就是专门测试创建一个API。这个API被赋予超级用户权限,允许测试代码可以忽视安全限制,绕过成本高昂的资源(比如数据库),强制将系统设置到某种可测试的状态中。总而言之,该API应该成为用户界面所用到的接口的一个超集。

设置测试API是为了将测试部分从应用程序中分离出来:这种解耦动作不仅是为了分隔测试部分与UI部分,而是要将测试代码与应用程序其他部分的代码结构分开。

第29章:整洁的嵌入式架构

软件应该是一种使用周期很长的东西,而固件则会随着硬件的演进而淘汰过时。虽然软件质量本身不会随着时间推移而损耗,但未妥善管理的硬件依赖和固件依赖却是软件的头号杀手。

软件构建三阶段

为什么很多嵌入式软件最后都成了固件?看起来,很可能是因为我们在做嵌入式设计时只关注代码能否顺利运行,并不太关心其结构是否能够撑起一个较长的有效生命周期。

Kent Beck(极限编程开创者)描述了软件构建过程的三个阶段:

  • 先让代码工作起来——如果代码不能工作,就不能产生价值;
  • 然后再试图将它变好——通过对代码进行重构,让我们自己和其他人更好地理解代码,并能够按照需求不断地修改代码;
  • 最后再试着让它运行得更快——按照性能提升的“需求”来重构代码。

这三个阶段层层递进,大部分人只关注第一个阶段,少部分人同时痴迷于第三个阶段(错误地越过了第二个阶段)。

第六部分:实现细节

第30章:数据库只是实现细节

从系统架构的角度讲,数据库并不重要——它只是一个实现细节。就数据库与整个系统架构的关系打个比方,它们之间就好比是门锁和整个房屋的关系。

这个比喻很是精妙,从建筑架构角度来讲门锁并不重要;但从安全角度讲,门锁很重要。

不过请注意,这里说的数据库是指存储数据的工具软件,不是指数据模型,数据结构(以及表结构)对系统架构来说是至关重要的。

为什么数据库系统如此流行

主要原因还是在于“硬盘”。硬盘的物理特性导致,文件系统只适合按块、顺序读写;为了支持按记录查找、读写数据,必须使用索引、缓存、查询优化等技术,这就是数据库系统产生的由来。

假设我们不再需要磁盘,或者磁盘变得和内存一样快会怎么样?那数据库(尤其是关系型数据库)的价值就没那么大了。我们将数据存储为链表、树、哈希表、堆栈、队列就行,这对程序员来说是最自然的方式。

想想Redis存储数据的方式。

那么性能怎么办?

性能难道不是系统架构的一个考量标准吗?是的,但当问题设计数据存储时,这方面的操作通常是被封装起来,隔离在业务逻辑之外。也就是说,我们确实需要从数据存储中快速地存取数据,但这终究只是一个底层实现问题。

性能是运行方面的问题,前面已经讨论过,性能不是架构关注的优先问题。

市场原因

数据库如此流行,以至于几乎每个系统都会配备数据库,还有另一层原因:市场原因——数据库厂商非常有效的市场推广,使得所有的IT高管相信“他们需要数据库”。

直到今天,这种市场的影响也无处不在,譬如“企业级”,“面向服务的架构”,“高并发”,“智能化”这样的宣传措辞。

不过话说回来,架构师在选择技术方案时考虑市场因素也是应该的。那些流行的技术方案成熟度更高,持续得到更新维护的概率更大,自然要优先考虑。但技术方案,始终要放在架构边缘。

第31章:Web是实现细节

本书出版之时,正是Web技术大行其道的年代,所以作者选取了这个标题。

GUI技术方案也只是实现细节,事实上,自互联网诞生起,中央服务器和用户终端之间的计算资源分配权重一直在不断震荡。自Web诞生以来,这样的震荡也发生了几次:一开始,我们认为计算资源应该集中在服务器,浏览器应该保持处理;但随后我们又发明了Applet,Ajax,JavaScript将很多计算挪回到浏览器中;后来又非常开心地采用Node技术将那些JavaScript代码挪回服务器上执行。无尽的钟摆!

因此,作为一名架构师,我们应该把眼光放长远一点,这些震荡只是短期问题,不应该把它们放在系统的核心业务逻辑中来考虑。从本质上看,Web(和其他终端形式)只是一种IO设备,众所周知,我们的系统应该是IO无关的。

在移动APP的年代,软件系统可能同时有两套GUI终端,使得这个问题更加重要。

第32章:应用程序框架也是实现细节

应用程序框架现在非常流程,这在通常情况下是一件好事。许多框架都非常有效,非常有用,而且是免费的。但框架并不等同于系统架构——尽管有些框架确实以此为目标,但实际上这并不现实,原因如下。

框架作者解决自己的问题,不是你的问题

框架作者原意免费提供自己的工作成果,是因为想要帮助社群、回馈社会,这值得鼓励。但不管动机有多高尚,他们恐怕也没有提供针对你个人的最佳方案,即使他们想,也做不到。

当然,你所遇到的问题和其他人大体上一致,否则框架也不会那么流行了。但是我们要清楚,我们与框架作者之间的关系是不对等的,我们采用一个框架就意味着自己要遵守一大堆约定,但框架作者却完全不需要为我们遵守什么约定。

使用框架的风险

对框架作者来说,应用程序与自己的框架耦合是没有什么风险的,因为他拥有绝对的控制权。而对你则不一样,至少面临以下几项风险。

  • 框架自生的设计很多时候并不是特别正确的

框架本省经常违反依赖关系原则,要求我们将框架代码引入到业务对象中。框架总会倾向于让我们将框架耦合在最内圈代码中,这样我们就再也离不开它了。

  • 框架不能满足需求

在早期,框架可能帮助我们快速地实现功能,但随着产品的成熟,功能要求很有可能超出框架所能提供的范围。而且随着时间的推移,我们可能会发现,自己与框架的斗争时间正在逐渐追赶升值超过框架帮助我们的时间。

  • 框架的演进方向

框架本身可能朝着我们不需要的方向演进,导致你在面临新版本的时候进退两难:一方面升级可以更好地适配其他技术(比如java的新版本),另一方面升级导致大量原有功能兼容性问题。

解决方案

一句话:不要嫁给框架!(不要和框架绑死)。

我们可以使用框架,但要时刻警惕,别被它拖住。我们应该将框架作为架构的最外圈的一个实现细节来使用,不要让它们进入内圈。

如果框架要求我们从它们提供的基类创建派生类,尽量不要这样做!我们可以创建一些代理类来适配框架需求,同时把这些代理类作为业务逻辑的插件使用。

本人使用Spring的理念:只使用依赖注入功能,其他的尽量不用;Spring Web MVC,只使用url映射功能,其它的尽量不用。

第33章 案例分析

第34章 拾遗

你可能感兴趣的:(架构设计,java,架构)