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

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值