四时宝库

程序员的知识宝库

架构整洁之道—软件架构

什么是软件架构

软件架构师其实应该是能力最强的一群程序员,并且必须一直坚持做一线程序员,他们通常会在自身承接编程任务的同时,逐渐引导整个团队向一个能够最大化生产力的系统设计方向前进。


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

而设计软件架构的目的,就是为了在工作中更好地对这些组件进行研发、部署、运行以及维护

如果想设计一个便于推进各项工作的系统,其策略就是要在设计中尽可能长时间地保留尽可能多的可选项


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


优秀的架构师会小心地将软件的高层策略与其底层实现隔离开,让高层策略与实现细节脱钩,使其策略部分完全不需要关心底层细节,当然也不会对这些细节有任何形式的依赖。另外,优秀的架构师所设计的策略应该允许系统尽可能地推迟与实现细节相关的决策,越晚做决策越好。


独立性

一个设计良好的软件架构必须支持以下几点:

系统的用例与正常运行。

系统的维护。

系统的开发。

系统的部署。

康威定律:

任何一个组织在设计系统时,往往都会复制出一个与该组织内沟通结构相同的系统。

保留可选项

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

按层解耦

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

用例的解耦

如果我们按照变更原因的不同对系统进行解耦,就可以持续地向系统内添加新的用例,而不会影响旧有的用例。如果我们同时对支持这些用例的UI和数据库也进行了分组,那么每个用例使用的就是不同面向的UI与数据库,因此增加新用例就更不太可能会影响旧有的用例了。

解耦的模式

两个目标:开发的独立性、部署的独立性

源码层次:我们可以控制源代码模块之间的依赖关系,以此来实现一个模块的变更不会导致其他模块也需要变更或重新编译(例如Ruby Gem)。

部署层次:我们可以控制部署单元(譬如jar文件、DLL、共享库等)之间的依赖关系,以此来实现一个模块的变更不会导致其他模块的重新构建和部署。

服务层次:我们可以将组件间的依赖关系降低到数据结构级别,然后仅通过网络数据包来进行通信。这样系统的每个执行单元在源码层和二进制层都会是一个独立的个体,它们的变更不会影响其他地方(例如,常见的服务或微服务就都是如此的)。

划分边界

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

插件式架构


由于用户界面在这个设计中是以插件形式存在的,所以我们可以用插拔的方式切换很多不同类型的用户界面。可以是基于Web模式的、基于客户端/服务器端模式的、基于SOA模式的、基于命令行模式的或者基于其他任何类型的用户界面技术的。

数据库也类似。因为我们现在是将数据库作为插件来对待的,所以它就可以被替换成不同类型的SQL数据库、NoSQL数据库,甚至基于文件系统的数据库,以及未来任何一种我们认为有必要发展的数据库技术。


软件开发技术发展的历史就是一个如何想方设法方便地增加插件,从而构建一个可扩展、可维护的系统架构的故事。


为了在软件架构中画边界线,我们需要先将系统分割成组件,其中一部分是系统的核心业务逻辑组件,而另一部分则是与核心业务逻辑无关但负责提供必要功能的插件。

策略与层次

在大多数非小型系统(nontrivial system)中,整体业务策略通常都可以被拆解为多组更小的策略语句。一部分策略语句专门用于描述计算部分的业务逻辑,另一部分策略语句则负责描述计算报告的格式。除此之外,可能还会有一些用于描述如何校验输入数据的策略。

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


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

通过将策略隔离,并让源码中的依赖方向都统一调整为指向高层策略,我们可以大幅度降低系统变更所带来的影响。因为一些针对系统低层组件的紧急小修改几乎不会影响系统中更高级、更重要的组件。

业务逻辑

严格地讲,业务逻辑就是程序中那些真正用于赚钱或省钱的业务逻辑与过程。


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


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


用例并不描述系统与用户之间的接口,它只描述该应用在某些特定情景下的业务逻辑,这些业务逻辑所规范的是用户与业务实体之间的交互方式,它与数据流入/流出系统的方式无关。


业务逻辑应该是系统中最独立、复用性最高的代码。

尖叫的软件架构

架构设计的主题

软件的系统架构应该为该系统的用例提供支持。

架构设计不是(或者说不应该是)与框架相关的,这件事不应该是基于框架来完成的。对于我们来说,框架只是一个可用的工具和手段,而不是一个架构所规范的内容。


架构设计的核心目标

一个良好的架构设计应该围绕着用例来展开,这样的架构设计可以在脱离框架、工具以及使用环境的情况下完整地描述用例。

良好的架构设计应该尽可能地允许用户推迟和延后决定采用什么框架、数据库、Web服务以及其他与环境相关的工具。


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

我们一定要带着怀疑的态度审视每一个框架。


可测试的架构设计

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


一个系统的架构应该着重于展示系统本身的设计,而并非该系统所使用的框架。

整洁架构

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


按照这些架构设计出来的系统,通常都具有以下特点:

独立于框架:这些系统的架构并不依赖某个功能丰富的框架之中的某个函数。框架可以被当成工具来使用,但不需要让系统来适应框架。

可被测试:这些系统的业务逻辑可以脱离UI、数据库、Web服务以及其他的外部元素来进行测试。

独立于UI:这些系统的UI变更起来很容易,不需要修改其他的系统部分。例如,我们可以在不修改业务逻辑的前提下将一个系统的UI由Web界面替换成命令行界面。

独立于数据库:我们可以轻易将这些系统使用的Oracle、SQL Server替换成Mongo、BigTable、CouchDB之类的数据库。因为业务逻辑与数据库之间已经完成了解耦。

独立于任何外部机构:这些系统的业务逻辑并不需要知道任何其他外部接口的存在。


依赖关系规则

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

业务实体

业务实体这一层中封装的是整个系统的关键业务逻辑,一个业务实体既可以是一个带有方法的对象,也可以是一组数据结构和函数的集合。

用例

软件的用例层中通常包含的是特定应用场景下的业务逻辑,这里面封装并实现了整个系统的所有用例。这些用例引导了数据在业务实体之间的流入/流出,并指挥着业务实体利用其中的关键业务逻辑来实现用例的设计目标。

接口适配器

软件的接口适配器层中通常是一组数据转换器,它们负责将数据从对用例和业务实体而言最方便操作的格式,转化成外部系统(譬如数据库以及Web)最方便操作的格式。

框架与驱动程序

框架与驱动程序层中包含了所有的实现细节。


通过将系统划分层次,并确保这些层次遵守依赖关系规则,就可以构建出一个天生可测试的系统,这其中的好处是不言而喻的。而且,当系统外层的这些数据库或Web框架过时的时候,我们还可以很轻松地替换它们。

展示器和谦卑对象

谦卑对象模式

谦卑对象模式最初的设计目的是帮助单元测试的编写者区分容易测试的行为与难以测试的行为,并将它们隔离。


展示器与视图

视图部分属于难以测试的谦卑对象。这种对象的代码通常应该越简单越好,它只应负责将数据填充到GUI上,而不应该对数据进行任何处理。

展示器则是可测试的对象。展示器的工作是负责从应用程序中接收数据,然后按视图的需要将这些数据格式化,以便视图将其呈现在屏幕上。


测试与架构

强大的可测试性是一个架构的设计是否优秀的显著衡量标准之一。

我们将系统行为分割成可测试和不可测试两部分的过程常常就也定义了系统的架构边界。


数据库网关

对于用例交互器(interactor)与数据库中间的组件,我们通常称之为数据库网关。


数据映射器

对象关系映射器(ORM)事实上是压根就不存在的。道理很简单,对象不是数据结构。从用户的角度来说,对象内部的数据应该都是私有的,不可见的,用户在通常情况下只能看到对象的公有函数。因此从用户角度来说,对象是一些操作的集合,而不是简单的数据结构体。

ORM更应该被称为“数据映射器”,因为它们只是将数据从关系型数据库加载到了对应的数据结构中。


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

不完全边界

省掉最后一步

构建不完全边界的一种方式就是在将系统分割成一系列可以独立编译、独立部署的组件之后,再把它们构建成一个组件。换句话说,在将系统中所有的接口、用于输入/输出的数据格式等每一件事都设置好之后,仍选择将它们统一编译和部署为一个组件。

单向边界

用反向接口来维护边界两侧组件的隔离性。

策略模式


门户模式

由Facade类来定义,这个类的背后是一份包含了所有服务函数的列表,它会负责将Client的调用传递给对Client不可见的服务函数。


架构师的职责之一就是预判未来哪里有可能会需要设置架构边界,并决定应该以完全形式还是不完全形式来实现它们。


层次与边界

基于文本的冒险游戏:Hunt The Wumpus

多个UI组件复用同一个游戏业务逻辑组件


遵循依赖关系规则的设计


整洁架构


简化版设计图


增加一个网络组件


管理玩家的高层策略


添加一个微服务的API


架构师必须持续观察系统的演进,时刻注意哪里可能需要设计边界,然后仔细观察这些地方会由于不存在边界而出现哪些问题。


Main组件

Main组件的任务是创建所有的工厂类、策略类以及其他的全局设施,并最终将系统的控制权转交给最高抽象层的代码来处理。

Main组件也可以被视为应用程序的一个插件——这个插件负责设置起始状态、配置信息、加载外部资源,最后将控制权转交给应用程序的其他高层组件。另外,由于Main组件能以插件形式存在于系统中,因此我们可以为一个系统设计多个Main组件,让它们各自对应于不同的配置。

服务:宏观与微观

所谓的服务本身只是一种比函数调用方式成本稍高的,分割应用程序行为的一种形式,与系统架构无关。


服务所带来的好处

解耦合的谬论

独立开发部署的谬论


运送猫咪的难题

出租车调度系统的服务架构图


在该城市开展猫咪送达服务的计划就是所谓的横跨型变更(cross-cutting concern)问题,它是所有的软件系统都要面对的问题,无论服务化还是非服务化的。


采用面向对象的方法来处理横跨型变更


该架构中的每个服务有自己内部的组件结构,允许以衍生类的方式为其添加新功能


系统的架构是由系统内部的架构边界,以及边界之间的依赖关系所定义的,与系统中各组件之间的调用和通信方式无关。


测试边界

测试也是一种系统组件

测试组件通常是一个系统中最独立的组件。系统的正常运行并不需要用到测试组件,用户也不依赖于测试组件。测试组件的存在是为了支持开发过程,而不是运行过程。


测试专用API

这个API应该被授予超级用户权限,允许测试代码可以忽视安全限制,绕过那些成本高昂的资源(例如数据库),强制将系统设置到某种可测试的状态中。


结构性耦合

假设我们现在有一组测试套件,它针对每个产品类都有一个对应的测试类,每个产品函数都有一个对应的测试函数。显然,该测试套件与应用程序在结构上是紧耦合的。


安全性

当然,这种具有超级权限的测试专用API如果被部署到我们的产品系统中,可能会是非常危险的。如果要避免这种情况发生,应该将测试专用API及其对应的具体实现放置在一个单独的、可独立部署的组件中。


测试并不是独立于整个系统之外的,恰恰相反,它们是系统的一个重要组成部分。

整洁的嵌入式架构

虽然软件质量本身并不会随时间推移而损耗,但是未妥善管理的硬件依赖和固件依赖却是软件的头号杀手。


软件构建过程中的三个阶段

1.“先让代码工作起来”——如果代码不能工作,就不能产生价值。

2.“然后再试图将它变好”——通过对代码进行重构,让我们自己和其他人更好地理解代码,并能按照需求不断地修改代码。

3.“最后再试着让它运行得更快”——按照性能提升的“需求”来重构代码。


目标硬件瓶颈(target-hardware bottleneck)是嵌入式开发所特有的一个问题,如果我们没有采用某种清晰的架构来设计嵌入式系统的代码结构,就经常会面临只能在目标系统平台上测试代码的难题。如果只能在特定的平台上测试代码,那么这一定会拖慢项目的开发进度。


软件与固件之间的边界被称为硬件抽象层(HAL)


HAL的作用是为软件部分提供一种服务,以便隐藏具体的实现细节。


分层架构的理念是基于接口编程的理念来设计的。当模块之间能以接口形式交互时,我们就可以将一个服务替换成另外一个服务。


推荐阅读《架构整洁之道》

个人博客:cyz

发表评论:

控制面板
您好,欢迎到访网站!
  查看权限
网站分类
最新留言
    友情链接