分离是为了更好的结合
写出高质量软件是困难和复杂的:不仅仅是为了满足需求,还应该是健壮的,可维护的,可测试的,并且足够灵活以适应成长和变化。这就是洋葱架构出现的原因,它代表一组优秀的开发实践,用来开发任何的软件应用都是一个不错的方式。
洋葱架构,也成为整洁架构(The Clean Architecture),用来构建具有如下特点的系统:
1. 独立的Frameworks
2. 可测试
3. 独立的UI
4. 独立的数据库
5. 独立的任意外部服务(代理)
看到这张图,你应该能理解为什么称其为洋葱架构了:D, 没错,这就是它的原理图。注意,并不是只能使用4个圆环,重点在于这里的依赖原则:代码依赖是从外向内的,内环中的代码不应该知道外环中的任何东西。
这里有一些相关的词汇可以帮助更好的理解和熟悉这种方式:
Entities:应用的业务对象。
Use Casess: Use Casess协调(Orchestrate)数据从Entities的流入和流出,也被称为Interactors。
Interface Adapters:这个Adapter集为Use Casess和Entities把数据转换为方便使用的格式(如渲染展示在页面上),Presenters和Controllers属于这里。
Frameworks and Drivers:这是实现所有细节的地方:UI,Tools,Frameworks等。
下面用一张更生动图来辅助说明它的原理:
依赖原则(The dependency rules)
上面的同心圆代表软件的不同部分。总得来说,越往里面,代码级别越高。外层的圆是(实现)机制,而内层的圆是原则(Policies)。
让这个架构起作用的最主要原则是依赖原则。这个原则要求源码依赖只能指向内部。内部的圆不能知道外圆的任何事情。一般来说,外圆的声明(包括方法、类、变量或任何软件实体)不能被内圆引用。
同样的,外圆使用的数据格式不能被内圆使用,尤其是外圆中的Framework产生的格式。我们不想让外圆的任何东西影响内圆。
越往里面抽象级别越高,最外层的圆是低级别的具体细节。越往里面内容越抽象,并且封装更高级别的原则(Policies)。最里面的圆是最通用的。
Entities
Entities封装了企业级的业务规则。一个Entity可以是一个带方法的对象,也可以是一个数据结构和方法集。Entities可以被用于企业的其他应用。
如果你没有加入企业,而是仅仅在写一个简单的应用,那么这些Entities就是这个应用的业务对象。它们封装了最通用、最上层的原则。它们是最不容易改变的,即使外部的东西改变了。例如,你不想让这些对象受到页面导航、安全的影响。应用的任何操作变化都不应该影响Entities Layer。
Use Casess
这一层包含了应用特有的业务规则。它封装和实现了系统的所有用例。这些用例协调数据从entities的流入和流出,并且指导entities使用它们的企业级业务规则来达到用例的目标。
我们不希望这一层的改变影响到Entities,同时也不希望这一层被外层的改变影响,如外层的数据库,UI或者任何Frameworks的改变,这一层独立于这些关注点。
当然,我们确实期望应用的操作变化影响用例层。如果一个用例的细节改变,那么这一层的部分代码确实会受到影响。
Interface Adapters
这一层包含一个adapters set(数据适配器集),它们把适用于Use Casess和entities的数据转换为适用于外部服务(external agency,如Database或Web)的格式。 例如,这一层可以完全包括GUI的MVX架构,Presenters, Views和Controllers都属于这里。Models可能仅仅是从Controllers传到Use Casess的数据结构,然后从Use Casess返回给Presenters和Views。
这一层的数据会被转换,从适用于entities和Use Casess的格式转换到适用于所使用的持久化框架的格式(如数据库)。这个圆以内的代码不应该知道关于数据库的任何东西。如果是一个SQL数据库,那么所有的SQL应该被限制到这一层,并且通常来说是被限制到层中跟数据库有关的部分。
同样,这一层也需要一些其他必要的Adapter来把外部的数据格式(如来自于外部服务的格式),转换为适用于Use Casess和entities的格式。
Frameworks and Drivers.
最外面的一层通常由Frameworks和Tools组成,如Database,Web Framework等。一般来说,除了用于和内层圆交互的连接代码,你不会在这一层写很多代码。
这一层是实现所有细节的地方。Web和Database都是需要实现的细节。我们把这些东西放在外面以减轻来自于它们的伤害(即减轻对他们的依赖)。
跨界
在图的右下角是一个我们应该如何跨界的例子。它展示了Controllers、Presenters与下一层的Use Casess的交互。注意控制的流向,它开始于Controller,经过Use Casess,最终在Presenter中执行。同时也请注意Source Code依赖,它们每一个都指向内部的Use Casess。
我们通常用依赖倒置原则来解决这个明显的矛盾。比如,在Java这样的语言里,我们可以使用接口和继承关系在合适的地方让源码依赖与控制流反向来跨界。
例如,假设Use Cases需要访问Presenter,当然,不能是直接访问,不然会违反依赖原则,所以我们让内圆的Use Cases访问一个接口(如图中的Use Cases output port),然后外圆的Presenter实现这个接口。
在这个架构中,同样的技术也被用于跨越其他的边界。我们利用运行时多态来创建与控制流相反的SourceCode依赖以满足依赖原则,无论控制流是如何流向的。
通常跨界的数据都是简单的数据结构。你可以使用简单的结构或数据传输对象(Data Transfer Object)。这个数据可以简单的是方法调用的参数,你也可以把它包装到一个HashMap或者一个对象。最重要的是独立的、简单的数据结构才能跨越边界。不要投机取巧,如传输Entites或者Database rows。我们不想让这个数据结构有任何违反依赖原则的依赖。
例如,很多的数据库框架对于query返回一个方便的数据格式,我们可以称之为Row Structure,我们不想向内部传递这个row structure。这会让内圆知道外圆的内容而违反了依赖原则。
所以,我们应该以最适用于内圆使用的格式来传递跨界的数据。
总结
满足这些简单的原则并不难,并且会减少项目进程中很多头疼的问题。通过把软件分成几层,并且满足依赖原则,你将会创建一个本身就可测试的系统,同时还有其他的好处。当系统的任何外层部分(如Database,Web 框架)废弃的时候,你可以轻松的替换这些废弃的元素。
代码实践可以参考:https://github.com/android10/Android-CleanArchitecture/releases
本文大部分内容译自The-Clean-Architecture,其中加入了自己学习该架构时的理解,如有意见和建议,欢迎交流!
参考:
The-Clean-Architecture:http://blog.8thlight.com/uncle-bob/2012/08/13/the-clean-architecture.html
Architecting-Android-The-Clean-Way:http://fernandocejas.com/2014/09/03/architecting-android-the-clean-way/
Architecting Android…The evolution:http://fernandocejas.com/2015/07/18/architecting-android-the-evolution/