这篇文章是翻译(Uncle Bob Martin)的文章。原文在这:http://blog.8thlight.com/uncle-bob/2012/08/13/the-clean-architecture.html。这篇文章作者尝试使用简单的观点将各种架构的共通之处和最终目标说清楚。全文要说清楚的就是一件事“如何写出整洁的架构”。作者希望在架构系统的时候只需要秉持最简单的两个观点(分层和依赖规则)开发,就能开发出干净整洁的系统架构。
以下是译文
过去几年间有许多关于系统架构的观点。比如:
1 六角架构(Hexagonal Architecture )。 这种架构是由Alistair Cockburn提出的,并由Steve Freeman和Nat Pryce在他们的书《Growing Object Oriented Software 》中提到。
2 洋葱架构(Onion Architecture )。提出者是Jeffrey Palermo。
3 尖叫架构(Screaming Architecture)。提出者是Uncle Bob(就是这篇文章的作者)。
4 DCI 架构。提出者是James Coplien和Trygve Reenskaug。
5 BCE 架构。提出者是Ivar Jacobson。在他的书《Object Oriented Software Engineering: A Use-Case Driven Approach》中有大量提及对这种架构的说明。
虽然这些架构在细节处都有一些变化,但是实际上,它们是非常相似的。它们的目标是一样的,将各种实体间的关系进行分离。它们分离操作的方法也是一样的,采用软件分层的方式。它们分的层中至少有一个业务逻辑层,并且有其他的接口层。
每一种架构一定能在写系统的业务逻辑的时候有以下特征:
1 与框架的分离。
框架决不能依赖一些有限制特征的库。这样就能保证你能像使用手边的工具一样简单地使用这些框架,而不会让你的系统业务逻辑在使用前就有一些强制性的约束。
2 可测试性。
业务逻辑必须能独立测试,不需要UI,数据库,Web服务器或者一些其他的外部条件。
3 与UI的分离。
UI必须能非常容易独立地修改。而不能在改变UI的同时需要改变系统其他部分。比如当把系统的UI从Web UI改成控制台UI,你并不需要改变任何业务逻辑的代码。
4 与数据库的分离。
能很方便地在Oracle,SQL Server,Mongo DB,BigTable,CouchDB或者其他数据库中进行切换和改变。业务逻辑决不能依赖这些数据库。
5 与外部结构的分离。
系统的业务逻辑并不需要知道任何外部的结构。
文章最上面的图就是将所有这些架构进行统一概括,提取出统一的观点。
依赖规则(The Dependency Rule)
同心圆代表的是不同层级的软件代码。通常当你更深一步思考构造你的系统的时候,你的系统就会在更高的层级。最外层的圈代表的是机制级别的系统。最内层的代表的是策略级别的系统。
最重要的一条规则是依赖规则(The Dependency Rule)。这条规则说的是:代码依赖只能使由外向内。换句话说,内层结构的代码不能包含有任何外层结构的信息。尤其是一些外层结构的名称不应该被内层结构的代码提到,比如函数名,类名,变量名,或者其他的系统实体的名称。
同样的,外层的数据结构不应该被内层代码使用,特别是那些由外部框架生成的数据结构。我们并不希望外部结构的任何东西会影响到内部结构。
实体层(Entities)
实体是用来封装公司的业务规则的。一个实体可以是一个带方法的对象,也可以是一些数据结构和函数。只要实体能被公司的不同业务逻辑部件使用,实体的具体表现形式是无所谓的。
或许你并不是想写公司级的架构,而只是想写一个简单的应用,那么这里实体就是指的应用的业务逻辑对象。它们封装了最通用的规则,并且当外部环境变化的时候,这些实体是最不需要被变化的。举例来说,比如在增加翻页需求或者是安全需求的时候,这些实体是最不应该被改变的。没有任何具体的应用需要改变实体层。
用户实例层(Use Cases)
这一层的软件结构包含了具体的应用业务逻辑。它实现了所有的用户实例。这些用户的实例由流入实体的数据流和流出实体的数据流实现,这些用户实例使得内层的实体能依靠实体内定义的业务逻辑规则来完成系统的用户需求。
我们不希望用户实例层的任何改变会影响到实体层。我们同样也不希望用户实例层会被外部的结构层,比如UI、数据库或者任何公共的框架,的改变而影响。这层应该是独立于这些概念的。
当然,必然发生的是应用的业务逻辑被修改会影响到用户实例层的代码和结构。如果用户的需求改变了,这层的部分当然会被修改。
接口适配层(Interface Adapters)
这一层的软件结构的目的就是进行数据的转换,将便于用户实例和实体层操作的数据结构变化成为最便于外部结构(比如数据库或者Web)操作的数据结构。比如GUI的MVC结构,表现器、视图器、控制器都是属于这个结构的。这层很可能是通过控制器将数据结构传给用户实例层,并且返回数据给表现器,视图器。
数据在这层会被转换,将便于实体层和用户实例层使用的数据转化成为持久层能使用的数据,比如数据库。这一层的代码并不需要知道任何数据库的信息。如果数据库是SQL数据库,那么,所有的SQL语言应当在这层被限制使用,特别是在这一层中与数据库有交互的代码部分。
当一些外部的服务需要与用户实例层和实体层进行交互的时候,这时候需要的数据转换也理所当然地放在这一层了。
框架和驱动层(Frameworks and Drivers)
最外层是由框架和使用工具组成的。比如数据库,Web框架等。通常你并不需要写很多代码就能达到与内层进行交互的行为。
这层表达的是所有的数据应该具体最终到达的地方。Web是数据的最终到达地,数据库也是数据的最终到达地。我们把这些东西放在最外层,它们几乎对整个系统的架构造不成什么影响。
只有四层?
答案是否定的。这个分层模型是纲要性的。在具体实现中,你会发现你可能需要到比四层更多的层级。并没有任何规定说你必须只能实现这四个层级。但是,依赖规则(The Dependency Rule)是必须遵守的:代码依赖只能使由外向内。越向内,抽象程度越高,越向外,细节信息越多。当你越向内设计的时候,你需要设计越高层面的策略。内部层级比外部层级更抽象。
跨层调用
图片的右下角是一个如何跨层调用的例子。它演示了控制器和表现器如何和用户实例层进行交互。注意下流向,从控制器开始,通过用户实例层,然后向上执行到表现器。这里也需要注意它们的代码依赖,必须是向内依赖的。
我们通常使用依赖倒置原则(Dependency Inversion Principle)来处理这种情况。比如在JAVA中,我们会使用接口或者继承关系来保证代码会在制定的地方依赖倒置。
举例说明,假设用户实例需要调用表现器。但是这种调用是违背了依赖规则的:外部的逻辑名称不应该被内部提到。所以我们让用户实例层调用一个接口(这里叫做用户实例输出端口Use Case Output Port),然后让表现层在外部实现这个接口。
相同的方法可以使用在架构的所有跨层调用上。因此实际上我们能使用多种形式的代码依赖达到我们能让控制流按照依赖规则的反方向流动。
什么数据能跨层调用?
一般跨层调用的数据是简单的数据结构。你可以使用数据结构或者是简单的数据传输流,又或者可以通过函数的参数来进行传递。你也可以将数据封装到一个hashmap结构,或者一个对象中。数据传输最重要的事情是无依赖,简单。我们并不希望跨层传递的数据是实体,或者是数据表的行数据,理由是我们不希望数据有任何形式的违反依赖规则。
例如,许多数据库框架对query查询返回行结构,我们叫它RowStructure。我们并不希望将这个RowStructure传递给内层结构,因为这个违反了依赖规则,它会强制让内层结构了解外层的结构。
总结
遵守这些简单的规则并不难,但是它们会解决你很多头疼的问题。将系统分层,遵守依赖规则,你就会自然写出可测试的系统了,所有附带的好处也会实现了。当任何外部的系统是独立的,比如数据库、web框架,你就能以最小的代价将它们进行替换。
作者简介
Robert C. Martin (Uncle Bob) , 8th Light公司的Master Craftsman, 获奖作家,著名的演说家,uber software geek。
它的著作有:
Designing Object Oriented c++ Applications using the Booch Method
Patterns Languages of Program Design 3(程序设计的模式语言)
More c++ Gems
Extreme Programming in Practice(解析极限编程)
Agile Software Development: Principles, Patterns, and Practices(敏捷软件开发:原则、模式与实践)
uml for java Programmers(UML:JAVA程序员指南)
Clean Code (代码整洁之道)
The Clean Coder(编码整洁之道:专业程序员的行为准则)