国际惯例先从Uncle Bob的文章开始谈起:
Bob提取出来大部分架构所需要的准则:
根据这些共有的理念,bob尝试将它们整合到一个单一可执行的想法中。这就是clean架构,而图片中的同心圆是架构思想的体现,代码用一种依赖规则分离到洋葱状的层:内层不应该知道关于外层的东西,依赖应该从外到内。
uncle Bob文章以及翻译:http://www.cnblogs.com/wanpengcoder/p/3479322.html
阅读前请参考Fernando的项目:
https://github.com/android10/Android-CleanArchitecture
而根据Fernando的说法,将Clean架构的思想运用到Android项目中如下图所示:
框架和驱动,细节实现的地方,包括UI、框架、数据库等具体实现。
数据转换的地方(DataMapper),这里可以是Presenters(MVP),ViewModel(MVVM),Controller(MVC)等MVX结构,X所在的地方
也可以叫Interactors,在下面会具体讲解
业务对象
根据这些规则可以把工程分为三层,如图所示:
也就是MVX结构所对应的地方(MVC、MVP等),这里不处理UI以外的任何逻辑。
业务逻辑,use case实现的地方,在这里包含了use case(用例)以及Bussiness Objects(业务对象),按照洋葱图的依赖规则,这层属于最内层,也就是完全不依赖于外层。
描述了你的业务逻辑,是整个app中最核心的元素。举个最简单的例子,假如说我对你的app一无所知,我只需要看你的domain层的描述,就能完全知道你的app能做什么,完全不需要看其他层次,它规定了要做什么,至于怎么做怎么实现,这些具体的实现逻辑就是外层的事情了,因为按照Uncle bob的说法,越往内层抽象的等级越高,最外层通常是具体的实现细节。
你完全可以在app中建立多个Use Case,即使是一些很小看起来很蠢萌的逻辑,Domain层的大部分业务逻辑都是在Use Case中实现的。这里是一个纯java模块,不包含任何的Android依赖。
所有APP需要的数据都是通过这层的XXDataRepository(实现了Domain层接口的类)提供的,它使用了Repository Pattern,关于Repository Pattern你可以参考我的翻译文章
简要概括就是使用Repository将业务层与具体的查询逻辑分离,Repository的实现类主要职责就是存储查询数据,一个更简单的解释就是你可以把Repository的实现类当成是一个Java容器(Collections),可以从中获取或存储数据(add/remove/etc),他对数据源进行了抽象。Domain层提供接口而不关心Data层到底是如何实现的,Data层的Repository只需要实现相关接口提供相关服务,至于两者之间的细节关系下面会讲。
p-1
p-2
目前我所看到的文章都有一个共同的问题,如图所示,一部分文章的作者在讲解clean架构的依赖关系时竟然是根据这幅图片讲解的,这完全没有道理,可以说完全背离了Clean架构,所以当你看到文章作者摆出这幅图来讲解的时候,你就可以关闭网页了。
事实上无论是p-1还是p-2它们表达的不是所谓的依赖关系,而是展示了数据流的流动过程。牢记各层之间的关系依旧是Uncle Bob的洋葱图,下图展示了架构的分层情况,并且说明了层次间的依赖关系:由外到内。最里面两层是核心的业务逻辑(Domain),这层完全定义了你的app的运行机制而data层和Presentation层是在洋葱的外层,所以在示例程序中,data层实际上拥有着domain层的引用。可以从gradle中清楚的看到依赖关系。
拿数据库举个例子,数据库不属于内圈的任何一层,它怎么去存储数据我们(业务逻辑)并不关心.数据库可以是原生的sqlite、realm或者其他任何方式,从架构的层次来说,这不重要。这就是为什么他在洋葱圈的边缘,假如你的外部存储系统上存在大量的依赖关系,那么在替换它的时候你将明白什么叫恐惧。
Domain层包含了数据接口(Repository接口)并且给了这些接口一个定义,接口表示我们怎么去存储和访问数据,这些就是业务逻辑,但是具体的实现与业务逻辑无关,也就是说Repository接口的实现与Domain层没有任何关系,它应该在data层做具体的实现,Domain层对于Data层是怎么实现的一无所知。
从洋葱图来看,Uncle Bob应该表达的是越往外层,具体的实现逻辑越容易被替换。内层诠释了你的应用程序是如何工作的,所以它们很少改变,只有当业务规则改变的时候才会发生变化。但是外层相对来说更容易通过某些情况引起变化:数据访问更改,网络接口改变,新的安卓版本带来的变化等等。。。
所以根本上从“n-层”架构区分,p-2的图会引起很大的误解,clean架构更像是一个洋葱架构。domain层保持独立并通过接口运转程序,这一部分可以理解为DIP(依赖倒置)。
在分析了架构的各层依赖关系以后,我们通过具体的例子来分析数据是怎么流动的,这能更好的帮助我们理解整个机制。
p-3
举个例子,比如说从Presenter层传递一个对象UserModel给Data层进行存储:
Presenter层:
Domain层(唯一的目的就是执行上面的业务逻辑:存储对象):
Data层:
这个问题实际上困扰了我一段时间,Domain层构建了Repository的接口,定义了需要实现的逻辑(方法),而data层接上接口做出具体实现,那么Domain层似乎是持有了data层的实现对象的引用啊?这不破坏依赖关系吗?
在仔细查看Uncle Bob的文章后,他也提出了这么一点:
We usually resolve this apparent contradiction by using theDependency Inversion Principle. In a language like Java, for example, we would arrange interfaces and inheritance relationships such that the source code dependencies oppose the flow of control at just the right points across the boundary.
For example, consider that the use case needs to call the presenter. However, this call must not be direct because that would violate The Dependency Rule: No name in an outer circle can be mentioned by an inner circle. So we have the use case call an interface (Shown here as Use Case Output Port) in the inner circle, and have the presenter in the outer circle implement it.
翻译是:我们通常使用依赖倒置规则来解决这个明显的矛盾。在一种语言中,比如Java,我们会安排接口和继承关系,这样源代码依赖可以反向控制流在恰到好处的点跨越边界。
比如,用例(Domain)需要调用persenter(View)。然而,这个不能直接调用,因为会违反依赖规则:外层环的任何名字都不能在内层环提及。所以在里层环我们使用用例调用接口(这里展现为Use Case Output Port),并且在外层环实现它。
Bob这里说的非常清晰,主要是依赖倒置(DIP)的软件设计原则来解决这种依赖关系:
1.由于抽象不依赖细节:在内层创建接口,而具体的实现在外层,Domain层对Data层是怎么实现的完全不知道,对于洋葱图来说,内层意味着抽象,外层意味着细节,同样一个抽象可能存在多个子类,这种1对多的方式更具灵活性,外层可以随意更换实现,这样也更符合开闭原则。
2.细节依赖抽象,业务逻辑层制订了规则,Data层等外层需要实现业务逻辑层的接口。这样才能保证在domain层能通过接口调用外层组件去实现需要的逻辑。
所以我认为从根本上来说,通过DIP淡化了依赖的概念,与其说他们之间具备依赖关系,不如说他们都只是依赖于抽象,使得外层被内层驱动,而内层并不关心外层具体的实现方法。Domain层制定抽象规则,Data层进行实现,Presentation层通过注入等方式将具体的实现对象注入Data。
关于DIP,IOC,DI可以参考这两篇文章:
http://www.uml.org.cn/sjms/201409021.asp
http://www.jianshu.com/p/c899300f98fa
对于一些特定的东西(持有Context、Service、Location、GCM notifications、特定的框架等)我们应该放在哪里呢?
首先从分析上来说,这些类都是一些具体的实现,容易被更换,并且大部分持有Context(Android相关的东西),从这层次来讲肯定是在外层,毕竟内层更倾向于抽象(但是这不意味着Domain层只是抽象,它同样可以拥有业务逻辑的某些具体实现,Domain层不仅仅是UseCase),也只能是纯粹的java代码,但是这些实现不一定与data相关。所以个人的见解就是,创建一个:Infrastructure layer。这一层从洋葱图架构来说和Data Layer处于同样的“层次”。Use Case可以同样的调用接口对外部组件进行控制,请牢记层次间的跨越关系,这将贯穿整个架构。
在测试方面,与示例的第一个版本相关的部分变化不大:
按照项目作者的说法,Repository不应该知道任何关于注册用户等(类似于api调用,返回一个Boolean变量)事件信息,他只是起到屏蔽数据源的作用,因此作者更倾向于实现一个独立的服务去实现“用户登录”等逻辑。其实这个问题不是架构本质上的问题,而是关于一些命名规范的问题。
类似于zhengxiaopeng的评论中说,在某些时候如果业务逻辑发生某些改变,那就意味着你的三层Model以及对应的Mapper都需要去更改,这样简直不可接受,改动量太大,并且有的时候各层之间的Model几乎是一样的,这意味着古板的复制黏贴代码,不符合DRY Principle(Dont Repeat Yourself)。所以按照zhengxiaopeng的意见来说,去除Presentation层的UserModel,只在Domain层和Data层保留相关代码,这样实际上没有破坏依赖规则。Presentation层可以获取Domain层对象的引用,在Presenter通过Mapper转换,将正确的对象提供给View去展示。我认为这是一个比较好的思路。相对来说,每层都定义一个model显得过于古板,对于大型程序来说代价也十分昂贵。
关于Stay(校长!)的看法:这算是一个必要的冗余,每一层间的数据传递都有对象的丰富与隐藏,用不同的object来指代更容易解耦。更具体的来说主要是因为手机端的use case基本上都是crud,太简单了,domain层没有发挥太大的作用,而如果这一层只是作为接头的中间层,是可以无限弱化,甚至删掉这一层的。也就跟p层合并了。
最重要的是:复杂的设计可以通过增加中间层来简化,反过来一样,如果设计很简单,那压根就不需要中间层。自己要掌握这个度。
Architecture is about intent, we have made it about frameworks and details。架构的核心在于目的,具体的框架、细节,要根据我们实际项目,实际的需求,做具体的实现。每个人对架构都有着不同的看法,以及具体的实施细节。但是当你使用Clean架构做为项目主架构的时候,请务必牢记洋葱图的依赖规则,以及各层之间的跨域规则,这将让你减少很多烦恼。最后用一张UML图结束这篇文章。
关于业务逻辑是什么你可以参考这个(我相信大部分人不知道):
http://www.uml.org.cn/zjjs/201008021.asp
关于如何向Use Case传递动态参数:
https://fernandocejas.com/2016/12/24/clean-architecture-dynamic-parameters-in-use-cases/
本文参考:
https://www.infoq.com/news/2013/07/architecture_intent_frameworks
https://github.com/hehonghui/android-tech-frontier/blob/master/issue-44/%E4%BD%BF%E7%94%A8Clean%20Architecture%E6%A8%A1%E5%9E%8B%E5%BC%80%E5%8F%91Android%E5%BA%94%E7%94%A8%E8%AF%A6%E7%BB%86%E6%8C%87%E5%8D%97.md
https://fernandocejas.com/2014/09/03/architecting-android-the-clean-way/
http://www.csdn.net/article/2015-08-20/2825506
http://stackoverflow.com/questions/35746546/android-mvp-what-is-an-interactor
http://www.cnblogs.com/wanpengcoder/p/3479322.html
架构宏观上的参考:
https://github.com/android10/Android-CleanArchitecture/issues/94
https://github.com/android10/Android-CleanArchitecture/issues/72
https://github.com/android10/Android-CleanArchitecture/issues/158
https://github.com/android10/Android-CleanArchitecture/issues/105
https://github.com/android10/Android-CleanArchitecture/issues/207
https://github.com/android10/Android-CleanArchitecture/issues/55
https://github.com/android10/Android-CleanArchitecture/issues/141
https://github.com/android10/Android-CleanArchitecture/issues/32
Android FrameWork Integrations参考:
https://github.com/android10/Android-CleanArchitecture/issues/115
https://github.com/android10/Android-CleanArchitecture/issues/47
https://github.com/android10/Android-CleanArchitecture/issues/127
https://github.com/android10/Android-CleanArchitecture/issues/151
关于依赖关系的参考:
https://github.com/android10/Android-CleanArchitecture/issues/136
https://github.com/android10/Android-CleanArchitecture/issues/150
https://github.com/android10/Android-CleanArchitecture/issues/65
https://github.com/android10/Android-CleanArchitecture/issues/143