
- 六边形架构(Hexagonal Architecture,也称端口与适配器架构),由Alistair Cockburn提出,Steve Freeman和Nat Pryce在他们的著作《Growing Object Oriented Software》中采用和推广;
- 洋葱架构(Onion Architecture),由Jeffrey Palermo提出;
- “尖叫架构”(Screaming Architecture),来自我去年写的一篇博客;
- DCI架构,由James Coplien和Trygve Reenskaug提出;
- BCE架构,由Ivar Jacobson在其著作《面向对象软件工程:以用例驱动的方法》中提出。
虽然这些架构在细节上各有不同,但它们非常相似。它们的共同目标是实现关注点分离,都是通过将软件划分为多个层次来实现的。每个架构至少包含一层业务规则层和一层接口层。
这些架构设计出来的系统具有以下特性:
- 独立于框架。架构不依赖于某个功能繁多的库或框架,这使得你可以将框架作为工具使用,而不是被迫将系统限制在框架的局限之中。
- 可测试性强。业务规则可以在没有UI、数据库、Web服务器或任何外部元素的情况下进行测试。
- 独立于UI。UI可以轻松更换,而不影响系统的其他部分。例如,可以用控制台UI替换Web UI,而无需改动业务规则。
- 独立于数据库。可以随意替换Oracle、SQL Server、Mongo、BigTable、CouchDB等数据库,业务规则不受数据库的限制。
- 独立于任何外部机构。业务规则本身对外部世界毫不知情。
本文开头的图示试图将上述所有架构整合成一个可操作的统一理念。
依赖规则(The Dependency Rule)
同心圆表示软件的不同区域。一般来说,越往内层,软件的抽象层级越高;外层是机制层,内层是策略层。
使该架构得以运作的核心规则是“依赖规则”:源码依赖只能指向内层。内层代码绝不能知道外层的任何东西,尤其不能引用外层声明的任何名称(函数、类、变量或其他命名的软件实体)。
同样,外层使用的数据格式不应被内层使用,尤其是那些由外层框架生成的数据格式。我们不希望外层的任何东西影响内层。
实体(Entities)
实体封装企业范围内的业务规则。实体可以是带有方法的对象,也可以是一组数据结构和函数,只要这些实体能被企业中的多个应用程序使用即可。
如果你没有企业背景,只是写一个单独的应用,那么这些实体就是应用的业务对象。它们封装最通用、最高层次的规则,最不容易因外部变化而变化。例如页面导航或安全性变动,不应影响实体层。
用例(Use Cases)
这一层的软件包含应用特定的业务规则,封装并实现系统的所有用例。用例负责协调数据在实体之间的流动,并指挥实体利用企业级业务规则完成用例目标。
预期这一层的变动不会影响实体层,也不会因数据库、UI或任何常用框架的变化而受影响。此层与这些外部因素隔离。
然而,应用操作的变更会影响用例层的代码。如果用例细节发生变化,这层代码必然需要相应调整。
接口适配器(Interface Adapters)
这一层是一组适配器,将数据从用例和实体最方便的格式转换为外部机构(如数据库或Web)最方便的格式。
例如,MVC架构中的控制器、视图和展示器都属于这一层。模型通常是控制器传递给用例,再由用例传给展示器和视图的数据结构。
同样,数据从实体和用例的格式转换为数据库所用的格式(如SQL)也在这里完成。内层代码绝不应知道数据库细节,所有SQL代码应限制在此层中与数据库相关的部分。
此外,任何将外部服务数据转换为内部用例和实体格式的适配器也在此层。
框架与驱动(Frameworks and Drivers)
最外层通常由数据库、Web框架等工具和框架组成。通常你只需在这一层写少量“胶水代码”,用于与内层通信。
这一层是所有具体细节所在。Web、数据库都是细节,我们将它们放在外层,以减少对系统核心的影响。
只有四层吗?
不一定,这些同心圆只是示意图。你可能需要比这更多的层。没有规定必须只有这四层。
但“依赖规则”始终适用:源码依赖永远指向内层。向内层越深,抽象层级越高。最外层是低层次的具体细节,最内层是最通用的高层策略。
跨越边界
图右下角展示了如何跨越层级边界。控制器和展示器与用例层通信,控制流从控制器开始,通过用例,最终执行展示器。
注意源码依赖方向,始终指向内层。
这看似矛盾,我们通常利用依赖倒置原则解决。在Java等语言中,通过接口和继承关系,使源码依赖方向与控制流方向相反,从而满足依赖规则。
例如,用例需要调用展示器,但不能直接调用(否则违反依赖规则)。解决方式是用例调用内层接口(用例输出端口),由外层的展示器实现该接口。
所有层级边界的跨越都采用类似技术,利用动态多态实现依赖方向与控制流方向的反向,从而无论控制流如何,都能遵守依赖规则。
通过边界传递的数据
通常跨层传递的是简单数据结构。可以是基本结构体、简单的数据传输对象(DTO),也可以是函数参数,或者封装在哈希表、对象中。
关键是传递的结构必须是孤立且简单的,不能传递实体或数据库行对象,避免违反依赖规则。
例如,许多数据库框架返回方便的查询结果格式(如行结构RowStructure),我们不应将其传递到内层,否则内层代码就会依赖外层,实现依赖规则被破坏。
跨层传递的数据应始终采用对内层最方便的形式。
结语
遵守这些简单规则并不难,却能为你今后的开发省去大量麻烦。通过分层设计,遵守依赖规则,你将构建一个天生可测试的系统,带来诸多好处。
当系统的外部部分(如数据库或Web框架)过时时,你可以轻松替换它们,几乎不影响系统其他部分。