0%

简明架构

最近为一个项目开发软件,在实做过程中发现软件需求比较复杂:涉及多种对内对外应用接口,需要操作的数据和需要维护的状态种类多,各种操作较繁杂还牵扯到同步问题。在一边开发功能一边重构的过程中发现一套能很好适应当前开发需求的软件组织方式,本想写一篇总结文章,但在搜索整理资料的时候发现有前辈在 2012 年的一篇博客中已经提出了这样的软件设计模式(或称架构),于是在这里翻译这篇文章供参考。

原文地址: The Clean Architecture

Architecture

以下为译文。

在过去的几年间我们已经看到过各种各样关于系统架构的设想,如下:

  • 来自 Alistair Cockburn 的六边形架构(也称接口与适配),此架构被 Steve Freeman 与 Nat Pryce 在其合著的《测试驱动的面向对象软件开发》一书中采纳
  • 来自 Jeffrey Palermo 的洋葱架构
  • 我去年在博客中提出的令人惊叹的架构
  • 由 James Coplien 和 Trygve Reenskaug 提出的 DCI
  • Ivar Jacobson 在其所著的《实例驱动的面向对象软件工程》一书中提出的 BCE

虽然以上这些架构都在细节上有所不同,但在宏观上非常相似。它们有同一个的目标,关注对软件的拆分,通过将软件分解为不同的层次,以达到拆分的目的。每种架构都至少有一层用于描述业务规则,其他层次用于创建接口。

使用这些架构所产生的系统都有以下特点:

  1. 独立于框架。架构不依赖于功能丰富的软件库。这让你能够将软件框架作为工具使用,而不用为了使用框架将其塞入系统以满足框架的限制。
  2. 可测试。业务规则能够在没有界面、数据库、Web 服务器或其他元素的情况下被测试。
  3. 独立于界面。可以在不改变系统其余部分的情况下很容易地修改界面。例如,可以将 Web 界面改为命令行界面却不改变业务规则。
  4. 独立于数据库。你可以随时弃用 Oracle 或 SQL Server 的数据库改为使用 Mongo、BigTable、CouchDB 或者其他数据库。因为你的业务规则不与数据库绑定。
  5. 独立于任意外部机构。事实上你的业务规则根本对外部世界一无所知。

依赖规则

图中每一个同心圆表示不同领域的软件。通常,项目开发得越久软件所处的层级越高。外层圆是机制,内层是策略。

使这个架构能够正常运转而起到决定性作用的规则是依赖规则。这个规则描述了源代码的依赖关系只能由外部指向内部,处于内圈的代码对处于外圈的代码一无所知。特别的,在外圈定义的对象的名称必不能被处于内圈的代码提及。这些对象包括:函数、类、变量或者其他有名称的软件实体。

出于同样的原因,在外圈使用的格式化数据不应该在内圈被使用,特别是由某种软件框架产生的格式化数据。我们不希望外圈的任何事物影响到内圈。

实体

实体封装了企业范围内的业务规则。实体可以是携带方法的对象也可以是由数据结构和函数组成的集合。实体只要能被企业中不同的应用程序使用即可,具体是什么则无关紧要。

如果你没有开发企业级软件而是在写单个应用程序,那么这些实体可以是应用程序的业务对象,其中封装了最普遍最高层次的规则。如果架构的外围发生了改变那么实体将会是最后一个需要修改的。例如,你不希望当页面导航或者安全保护方面产生了变更而影响到实体。总的来说,任意指定程序的操作变更都不应影响实体层。

用例

处于这一层的软件包含面向应用程序的业务规则,封装和实现所有系统用例。这些用例编排数据流动包括流向实体和从实体流入,以及引导实体使用企业业务规则来实现用例的目的。

同样,我们不希望这一层的变更影响实体。也不希望这一层受到外部变更的影响,比如数据库、UI 或者任意软件框架,此层与这些情况无关。

可以预期,应用程序操作的变更会影响用例进而影响处于此层的软件。如果用例本身发生了变化,那么处于此层的代码一定会受到影响。

接口适配器

处于此层的软件通常是一组适配器。适配器的作用是将方便于用例和实体使用的数据格式转换为方便外部机构使用的数据格式。外部机构包括数据库、Web 等。比如在此层中会完全包含图形用户界面的 MVC 架构,表示器、视图以及控制器都属于此层。模型一般表示为数据结构,从控制器传给用例,再从用例返回到表示器和视图。

类似的,数据从方便用例和实体使用的形式转换到方便持久化框架(如数据库)使用的形式也发生在此层。从这一层向内的所有代码都对数据库不了解。如果数据是 SQL 数据库那么所有的 SQL 语句都应该被限制在此层用于与数据库交互的部分。

当然此层还有另一种必备的适配器,用于将来自外部服务的数据格式转换为用例和实体使用的内部数据格式。

框架和驱动

最外一层通常是由软件框架与工具组成,例如数据库、Web 开发框架等。一般你不需要在此层写大量代码而是写一些“胶水”代码用于与紧邻的内层通信。

这一层是所有具体细节出现的地方,例如 Web、数据库都充满各种细节需要关注,将这类事物放到最外层可以最大可能地减少对整体系统的影响。

仅有四层?

不是,此图只是概念展示。在实际操作中你会发现所需的不止四层,也没有规定一定只能有四层。无论怎样依赖原则始终有效,即源代码总是向内依赖。越往内越抽象。最外圈是最低层次包含最具体的细节,越往内软件越为抽象封装更高层次的策略,最中心的圈也就最一般化。

跨越边界

在图示中的右下角可以看到一个示例展示了我们如何跨越不同层次的边界。可以看到控制器和表示器通过它们隔壁层的用例来互相通信。注意控制的流向,它从控制器开始,通过用例最后在表示器中执行以结束。在看源代码的依赖方向,它们都向内指向用例,这就产生了矛盾。

通常我们使用依赖倒置原则解决这个显而易见的冲突。诸如 Java 一类的编程语言我们可以通过组织一组接口和类继承关系使得在适当的时候让源代码的依赖方向和控制方向相反以实现跨越边界。

例如,考虑当用例需要调用表示器的情况。这种调用方式必不能直接实现,因为这样违反了依赖原则:内部事物对外部一无所知。因此我们让用例定义以及调用接口(interface)也就是图中内圈的 Use Case Output Port,而让外圈的表示器实现这个接口(interface)。

这个技巧可以用在整个架构中所有需要跨越边界的地方。我们利用面向对象编程语言的动态多态性这个特点创建与控制流向相反的源代码依赖。这样不论控制流动的方向是什么我们都能让设计符合依赖原则

哪些数据会跨越边界

一般跨越边界的是简单的数据结构。可以根据自己的需要选择基本数据结构或者简单的数据传输对象或者函数调用时传入的参数,也可以将数据打包进哈稀表或者构建到对象里,关键在于用于跨越边界的数据要足够简单和具备独立性。我们并不希望让实体或数据库的原始数据行跨越边界,同样不希望跨越边界的结构里包含任意会违背依赖原则的数据。

比如,许多数据库框架会用便于使用的数据格式作为查询的响应,我们把这样的数据称为 RowStructure。我们不希望跨越边界时传递这样的结构,这样可能会违背依赖原则因为这有几率迫使内部的代码必须了解外部的数据定义。

因此,我们传递的数据跨越边界时,最常用是便于内层使用的格式。

总结

遵循这些简单的规则并不费劲,并且可以省去开发过程中很多麻烦。将整体软件分层并配合依赖原则,你可以创建一个具备极好测试性的系统,这会带来非常多的好处。当系统外部任意部分成为了整体系统的瓶颈,如数据库或者 Web 开发框架,你能只花很小的代价就将其撤换。

Welcome to my other publishing channels