Clean架构探讨

国际惯例先从Uncle Bob的文章开始谈起:

Bob提取出来大部分架构所需要的准则:

  • 框架独立。架构不依赖于一些满载功能的软件库。
  • 可测试性。
  • UI独立,在不改变系统其余部分的情况下完成对UI的简易更改。
  • 数据库独立,业务规则不绑定与某个具体的数据库当中,可以随意更换数据库的具体实现:比如说从SQL换到BigTable,这种情况不会对业务规则产生影响。
  • 外部机制独立,业务规则完全不知道外层的事情。

根据这些共有的理念,bob尝试将它们整合到一个单一可执行的想法中。这就是clean架构,而图片中的同心圆是架构思想的体现,代码用一种依赖规则分离到洋葱状的层:内层不应该知道关于外层的东西,依赖应该从外到内。

uncle Bob文章以及翻译:http://www.cnblogs.com/wanpengcoder/p/3479322.html

阅读前请参考Fernando的项目:
https://github.com/android10/Android-CleanArchitecture

而根据Fernando的说法,将Clean架构的思想运用到Android项目中如下图所示:

Framework and Drivers:

框架和驱动,细节实现的地方,包括UI、框架、数据库等具体实现。

Interface Adapter:

数据转换的地方(DataMapper),这里可以是Presenters(MVP),ViewModel(MVVM),Controller(MVC)等MVX结构,X所在的地方

Use Cases:

也可以叫Interactors,在下面会具体讲解

Entity:

业务对象

根据这些规则可以把工程分为三层,如图所示:

Presentation Layer:

也就是MVX结构所对应的地方(MVC、MVP等),这里不处理UI以外的任何逻辑。

Domain Layer:

业务逻辑,use case实现的地方,在这里包含了use case(用例)以及Bussiness Objects(业务对象),按照洋葱图的依赖规则,这层属于最内层,也就是完全不依赖于外层。

Use Case:

描述了你的业务逻辑,是整个app中最核心的元素。举个最简单的例子,假如说我对你的app一无所知,我只需要看你的domain层的描述,就能完全知道你的app能做什么,完全不需要看其他层次,它规定了要做什么,至于怎么做怎么实现,这些具体的实现逻辑就是外层的事情了,因为按照Uncle bob的说法,越往内层抽象的等级越高,最外层通常是具体的实现细节。

你完全可以在app中建立多个Use Case,即使是一些很小看起来很蠢萌的逻辑,Domain层的大部分业务逻辑都是在Use Case中实现的。这里是一个纯java模块,不包含任何的Android依赖。

Data Layer:

所有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层:

  • 用户输入数据,并点击OK按钮(View)
  • Presenter(ViewModel,Controller等同样)获取到数据,并构造一个UserModel
  • 使用UserModelMapper(Presenter层的数据Mapper对象)将UserModel转换成User对象
  • 调用UseCase.store(user)

Domain层(唯一的目的就是执行上面的业务逻辑:存储对象):

  • StoreUseCase接受到User对象
  • (这里可以先做额外的逻辑)
  • 调用UserRepository接口的方法,传入User

Data层:

  • UserDataRepository(UserRepository接口的实现类),接受到User对象
  • 调用Mapper方法(Data层)将User对象转换成UserEntity
  • 存储UserEntity对象
  • 这样可以清楚的看到数据的流动过程,从左往右,但是请在你脑海里铭记这一点,这只是数据的流动过程,与依赖关系无关。Domain层实际上不持有任何依赖。

细节探讨:

层次间跨越关系:

这个问题实际上困扰了我一段时间,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

Android FrameWork集成:

对于一些特定的东西(持有Context、Service、Location、GCM notifications、特定的框架等)我们应该放在哪里呢?

首先从分析上来说,这些类都是一些具体的实现,容易被更换,并且大部分持有Context(Android相关的东西),从这层次来讲肯定是在外层,毕竟内层更倾向于抽象(但是这不意味着Domain层只是抽象,它同样可以拥有业务逻辑的某些具体实现,Domain层不仅仅是UseCase),也只能是纯粹的java代码,但是这些实现不一定与data相关。所以个人的见解就是,创建一个:Infrastructure layer。这一层从洋葱图架构来说和Data Layer处于同样的“层次”。Use Case可以同样的调用接口对外部组件进行控制,请牢记层次间的跨越关系,这将贯穿整个架构。

测试方法:

在测试方面,与示例的第一个版本相关的部分变化不大:

  • 表现层:用Espresso 2和Android Instrumentation测试框架测试UI。
  • 领域层:JUnit + Mockito —— 它是Java的标准模块。
  • 数据层:将测试组合换成了Robolectric 3 + JUnit + Mockito。这一层的测试曾经存 在于单独的Android模块。由于当时(当前示例程序的第一个版本)没有内置单元测试的支 持,也没有建立像robolectric那样的框架,该框架比较复杂,需要一群黑客的帮忙才能让其 正常工作。

架构问题探讨:

Repository是否要做API调用等工作?

按照项目作者的说法,Repository不应该知道任何关于注册用户等(类似于api调用,返回一个Boolean变量)事件信息,他只是起到屏蔽数据源的作用,因此作者更倾向于实现一个独立的服务去实现“用户登录”等逻辑。其实这个问题不是架构本质上的问题,而是关于一些命名规范的问题。

在三层之间构造三个Model对象(UserModel,User,UserEntity),并且有着对应的Mapper,这是否有必要?

类似于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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,222评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,455评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,720评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,568评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,696评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,879评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,028评论 3 409
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,773评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,220评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,550评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,697评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,360评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,002评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,782评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,010评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,433评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,587评论 2 350

推荐阅读更多精彩内容