國(guó)際慣例先從Uncle Bob的文章開(kāi)始談起:
Bob提取出來(lái)大部分架構(gòu)所需要的準(zhǔn)則:
- 框架獨(dú)立。架構(gòu)不依賴于一些滿載功能的軟件庫(kù)。
- 可測(cè)試性。
- UI獨(dú)立,在不改變系統(tǒng)其余部分的情況下完成對(duì)UI的簡(jiǎn)易更改。
- 數(shù)據(jù)庫(kù)獨(dú)立,業(yè)務(wù)規(guī)則不綁定與某個(gè)具體的數(shù)據(jù)庫(kù)當(dāng)中,可以隨意更換數(shù)據(jù)庫(kù)的具體實(shí)現(xiàn):比如說(shuō)從SQL換到BigTable,這種情況不會(huì)對(duì)業(yè)務(wù)規(guī)則產(chǎn)生影響。
- 外部機(jī)制獨(dú)立,業(yè)務(wù)規(guī)則完全不知道外層的事情。
根據(jù)這些共有的理念,bob嘗試將它們整合到一個(gè)單一可執(zhí)行的想法中。這就是clean架構(gòu),而圖片中的同心圓是架構(gòu)思想的體現(xiàn),代碼用一種依賴規(guī)則分離到洋蔥狀的層:內(nèi)層不應(yīng)該知道關(guān)于外層的東西,依賴應(yīng)該從外到內(nèi)。
uncle Bob文章以及翻譯:http://www.cnblogs.com/wanpengcoder/p/3479322.html
閱讀前請(qǐng)參考Fernando的項(xiàng)目:
https://github.com/android10/Android-CleanArchitecture
而根據(jù)Fernando的說(shuō)法,將Clean架構(gòu)的思想運(yùn)用到Android項(xiàng)目中如下圖所示:
Framework and Drivers:
框架和驅(qū)動(dòng),細(xì)節(jié)實(shí)現(xiàn)的地方,包括UI、框架、數(shù)據(jù)庫(kù)等具體實(shí)現(xiàn)。
Interface Adapter:
數(shù)據(jù)轉(zhuǎn)換的地方(DataMapper),這里可以是Presenters(MVP),ViewModel(MVVM),Controller(MVC)等MVX結(jié)構(gòu),X所在的地方
Use Cases:
也可以叫Interactors,在下面會(huì)具體講解
Entity:
業(yè)務(wù)對(duì)象
根據(jù)這些規(guī)則可以把工程分為三層,如圖所示:
Presentation Layer:
也就是MVX結(jié)構(gòu)所對(duì)應(yīng)的地方(MVC、MVP等),這里不處理UI以外的任何邏輯。
Domain Layer:
業(yè)務(wù)邏輯,use case實(shí)現(xiàn)的地方,在這里包含了use case(用例)以及Bussiness Objects(業(yè)務(wù)對(duì)象),按照洋蔥圖的依賴規(guī)則,這層屬于最內(nèi)層,也就是完全不依賴于外層。
Use Case:
描述了你的業(yè)務(wù)邏輯,是整個(gè)app中最核心的元素。舉個(gè)最簡(jiǎn)單的例子,假如說(shuō)我對(duì)你的app一無(wú)所知,我只需要看你的domain層的描述,就能完全知道你的app能做什么,完全不需要看其他層次,它規(guī)定了要做什么,至于怎么做怎么實(shí)現(xiàn),這些具體的實(shí)現(xiàn)邏輯就是外層的事情了,因?yàn)榘凑誙ncle bob的說(shuō)法,越往內(nèi)層抽象的等級(jí)越高,最外層通常是具體的實(shí)現(xiàn)細(xì)節(jié)。
你完全可以在app中建立多個(gè)Use Case,即使是一些很小看起來(lái)很蠢萌的邏輯,Domain層的大部分業(yè)務(wù)邏輯都是在Use Case中實(shí)現(xiàn)的。這里是一個(gè)純java模塊,不包含任何的Android依賴。
Data Layer:
所有APP需要的數(shù)據(jù)都是通過(guò)這層的XXDataRepository(實(shí)現(xiàn)了Domain層接口的類(lèi))提供的,它使用了Repository Pattern,關(guān)于Repository Pattern你可以參考我的翻譯文章
簡(jiǎn)要概括就是使用Repository將業(yè)務(wù)層與具體的查詢邏輯分離,Repository的實(shí)現(xiàn)類(lèi)主要職責(zé)就是存儲(chǔ)查詢數(shù)據(jù),一個(gè)更簡(jiǎn)單的解釋就是你可以把Repository的實(shí)現(xiàn)類(lèi)當(dāng)成是一個(gè)Java容器(Collections),可以從中獲取或存儲(chǔ)數(shù)據(jù)(add/remove/etc),他對(duì)數(shù)據(jù)源進(jìn)行了抽象。Domain層提供接口而不關(guān)心Data層到底是如何實(shí)現(xiàn)的,Data層的Repository只需要實(shí)現(xiàn)相關(guān)接口提供相關(guān)服務(wù),至于兩者之間的細(xì)節(jié)關(guān)系下面會(huì)講。
具體的依賴與這三層之間的關(guān)系:
目前我所看到的文章都有一個(gè)共同的問(wèn)題,如圖所示,一部分文章的作者在講解clean架構(gòu)的依賴關(guān)系時(shí)竟然是根據(jù)這幅圖片講解的,這完全沒(méi)有道理,可以說(shuō)完全背離了Clean架構(gòu),所以當(dāng)你看到文章作者擺出這幅圖來(lái)講解的時(shí)候,你就可以關(guān)閉網(wǎng)頁(yè)了。
事實(shí)上無(wú)論是p-1還是p-2它們表達(dá)的不是所謂的依賴關(guān)系,而是展示了數(shù)據(jù)流的流動(dòng)過(guò)程。牢記各層之間的關(guān)系依舊是Uncle Bob的洋蔥圖,下圖展示了架構(gòu)的分層情況,并且說(shuō)明了層次間的依賴關(guān)系:由外到內(nèi)。最里面兩層是核心的業(yè)務(wù)邏輯(Domain),這層完全定義了你的app的運(yùn)行機(jī)制而data層和Presentation層是在洋蔥的外層,所以在示例程序中,data層實(shí)際上擁有著domain層的引用。可以從gradle中清楚的看到依賴關(guān)系。
拿數(shù)據(jù)庫(kù)舉個(gè)例子,數(shù)據(jù)庫(kù)不屬于內(nèi)圈的任何一層,它怎么去存儲(chǔ)數(shù)據(jù)我們(業(yè)務(wù)邏輯)并不關(guān)心.數(shù)據(jù)庫(kù)可以是原生的sqlite、realm或者其他任何方式,從架構(gòu)的層次來(lái)說(shuō),這不重要。這就是為什么他在洋蔥圈的邊緣,假如你的外部存儲(chǔ)系統(tǒng)上存在大量的依賴關(guān)系,那么在替換它的時(shí)候你將明白什么叫恐懼。
Domain層包含了數(shù)據(jù)接口(Repository接口)并且給了這些接口一個(gè)定義,接口表示我們?cè)趺慈ゴ鎯?chǔ)和訪問(wèn)數(shù)據(jù),這些就是業(yè)務(wù)邏輯,但是具體的實(shí)現(xiàn)與業(yè)務(wù)邏輯無(wú)關(guān),也就是說(shuō)Repository接口的實(shí)現(xiàn)與Domain層沒(méi)有任何關(guān)系,它應(yīng)該在data層做具體的實(shí)現(xiàn),Domain層對(duì)于Data層是怎么實(shí)現(xiàn)的一無(wú)所知。
從洋蔥圖來(lái)看,Uncle Bob應(yīng)該表達(dá)的是越往外層,具體的實(shí)現(xiàn)邏輯越容易被替換。內(nèi)層詮釋了你的應(yīng)用程序是如何工作的,所以它們很少改變,只有當(dāng)業(yè)務(wù)規(guī)則改變的時(shí)候才會(huì)發(fā)生變化。但是外層相對(duì)來(lái)說(shuō)更容易通過(guò)某些情況引起變化:數(shù)據(jù)訪問(wèn)更改,網(wǎng)絡(luò)接口改變,新的安卓版本帶來(lái)的變化等等。。。
所以根本上從“n-層”架構(gòu)區(qū)分,p-2的圖會(huì)引起很大的誤解,clean架構(gòu)更像是一個(gè)洋蔥架構(gòu)。domain層保持獨(dú)立并通過(guò)接口運(yùn)轉(zhuǎn)程序,這一部分可以理解為DIP(依賴倒置)。
數(shù)據(jù)流到底是怎么流動(dòng)的:
在分析了架構(gòu)的各層依賴關(guān)系以后,我們通過(guò)具體的例子來(lái)分析數(shù)據(jù)是怎么流動(dòng)的,這能更好的幫助我們理解整個(gè)機(jī)制。
舉個(gè)例子,比如說(shuō)從Presenter層傳遞一個(gè)對(duì)象UserModel給Data層進(jìn)行存儲(chǔ):
Presenter層:
- 用戶輸入數(shù)據(jù),并點(diǎn)擊OK按鈕(View)
- Presenter(ViewModel,Controller等同樣)獲取到數(shù)據(jù),并構(gòu)造一個(gè)UserModel
- 使用UserModelMapper(Presenter層的數(shù)據(jù)Mapper對(duì)象)將UserModel轉(zhuǎn)換成User對(duì)象
- 調(diào)用UseCase.store(user)
Domain層(唯一的目的就是執(zhí)行上面的業(yè)務(wù)邏輯:存儲(chǔ)對(duì)象):
- StoreUseCase接受到User對(duì)象
- (這里可以先做額外的邏輯)
- 調(diào)用UserRepository接口的方法,傳入U(xiǎn)ser
Data層:
- UserDataRepository(UserRepository接口的實(shí)現(xiàn)類(lèi)),接受到User對(duì)象
- 調(diào)用Mapper方法(Data層)將User對(duì)象轉(zhuǎn)換成UserEntity
- 存儲(chǔ)UserEntity對(duì)象
- 這樣可以清楚的看到數(shù)據(jù)的流動(dòng)過(guò)程,從左往右,但是請(qǐng)?jiān)谀隳X海里銘記這一點(diǎn),這只是數(shù)據(jù)的流動(dòng)過(guò)程,與依賴關(guān)系無(wú)關(guān)。Domain層實(shí)際上不持有任何依賴。
細(xì)節(jié)探討:
層次間跨越關(guān)系:
這個(gè)問(wèn)題實(shí)際上困擾了我一段時(shí)間,Domain層構(gòu)建了Repository的接口,定義了需要實(shí)現(xiàn)的邏輯(方法),而data層接上接口做出具體實(shí)現(xiàn),那么Domain層似乎是持有了data層的實(shí)現(xiàn)對(duì)象的引用啊?這不破壞依賴關(guān)系嗎?
在仔細(xì)查看Uncle Bob的文章后,他也提出了這么一點(diǎn):
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.
翻譯是:我們通常使用依賴倒置規(guī)則來(lái)解決這個(gè)明顯的矛盾。在一種語(yǔ)言中,比如Java,我們會(huì)安排接口和繼承關(guān)系,這樣源代碼依賴可以反向控制流在恰到好處的點(diǎn)跨越邊界。
比如,用例(Domain)需要調(diào)用persenter(View)。然而,這個(gè)不能直接調(diào)用,因?yàn)闀?huì)違反依賴規(guī)則:外層環(huán)的任何名字都不能在內(nèi)層環(huán)提及。所以在里層環(huán)我們使用用例調(diào)用接口(這里展現(xiàn)為Use Case Output Port),并且在外層環(huán)實(shí)現(xiàn)它。
Bob這里說(shuō)的非常清晰,主要是依賴倒置(DIP)的軟件設(shè)計(jì)原則來(lái)解決這種依賴關(guān)系:
1.由于抽象不依賴細(xì)節(jié):在內(nèi)層創(chuàng)建接口,而具體的實(shí)現(xiàn)在外層,Domain層對(duì)Data層是怎么實(shí)現(xiàn)的完全不知道,對(duì)于洋蔥圖來(lái)說(shuō),內(nèi)層意味著抽象,外層意味著細(xì)節(jié),同樣一個(gè)抽象可能存在多個(gè)子類(lèi),這種1對(duì)多的方式更具靈活性,外層可以隨意更換實(shí)現(xiàn),這樣也更符合開(kāi)閉原則。
2.細(xì)節(jié)依賴抽象,業(yè)務(wù)邏輯層制訂了規(guī)則,Data層等外層需要實(shí)現(xiàn)業(yè)務(wù)邏輯層的接口。這樣才能保證在domain層能通過(guò)接口調(diào)用外層組件去實(shí)現(xiàn)需要的邏輯。
所以我認(rèn)為從根本上來(lái)說(shuō),通過(guò)DIP淡化了依賴的概念,與其說(shuō)他們之間具備依賴關(guān)系,不如說(shuō)他們都只是依賴于抽象,使得外層被內(nèi)層驅(qū)動(dòng),而內(nèi)層并不關(guān)心外層具體的實(shí)現(xiàn)方法。Domain層制定抽象規(guī)則,Data層進(jìn)行實(shí)現(xiàn),Presentation層通過(guò)注入等方式將具體的實(shí)現(xiàn)對(duì)象注入Data。
關(guān)于DIP,IOC,DI可以參考這兩篇文章:
http://www.uml.org.cn/sjms/201409021.asp
http://www.lxweimin.com/p/c899300f98fa
Android FrameWork集成:
對(duì)于一些特定的東西(持有Context、Service、Location、GCM notifications、特定的框架等)我們應(yīng)該放在哪里呢?
首先從分析上來(lái)說(shuō),這些類(lèi)都是一些具體的實(shí)現(xiàn),容易被更換,并且大部分持有Context(Android相關(guān)的東西),從這層次來(lái)講肯定是在外層,畢竟內(nèi)層更傾向于抽象(但是這不意味著Domain層只是抽象,它同樣可以擁有業(yè)務(wù)邏輯的某些具體實(shí)現(xiàn),Domain層不僅僅是UseCase),也只能是純粹的java代碼,但是這些實(shí)現(xiàn)不一定與data相關(guān)。所以個(gè)人的見(jiàn)解就是,創(chuàng)建一個(gè):Infrastructure layer。這一層從洋蔥圖架構(gòu)來(lái)說(shuō)和Data Layer處于同樣的“層次”。Use Case可以同樣的調(diào)用接口對(duì)外部組件進(jìn)行控制,請(qǐng)牢記層次間的跨越關(guān)系,這將貫穿整個(gè)架構(gòu)。
測(cè)試方法:
在測(cè)試方面,與示例的第一個(gè)版本相關(guān)的部分變化不大:
- 表現(xiàn)層:用Espresso 2和Android Instrumentation測(cè)試框架測(cè)試UI。
- 領(lǐng)域?qū)樱篔Unit + Mockito —— 它是Java的標(biāo)準(zhǔn)模塊。
- 數(shù)據(jù)層:將測(cè)試組合換成了Robolectric 3 + JUnit + Mockito。這一層的測(cè)試曾經(jīng)存 在于單獨(dú)的Android模塊。由于當(dāng)時(shí)(當(dāng)前示例程序的第一個(gè)版本)沒(méi)有內(nèi)置單元測(cè)試的支 持,也沒(méi)有建立像robolectric那樣的框架,該框架比較復(fù)雜,需要一群黑客的幫忙才能讓其 正常工作。
架構(gòu)問(wèn)題探討:
Repository是否要做API調(diào)用等工作?
按照項(xiàng)目作者的說(shuō)法,Repository不應(yīng)該知道任何關(guān)于注冊(cè)用戶等(類(lèi)似于api調(diào)用,返回一個(gè)Boolean變量)事件信息,他只是起到屏蔽數(shù)據(jù)源的作用,因此作者更傾向于實(shí)現(xiàn)一個(gè)獨(dú)立的服務(wù)去實(shí)現(xiàn)“用戶登錄”等邏輯。其實(shí)這個(gè)問(wèn)題不是架構(gòu)本質(zhì)上的問(wèn)題,而是關(guān)于一些命名規(guī)范的問(wèn)題。
在三層之間構(gòu)造三個(gè)Model對(duì)象(UserModel,User,UserEntity),并且有著對(duì)應(yīng)的Mapper,這是否有必要?
類(lèi)似于zhengxiaopeng的評(píng)論中說(shuō),在某些時(shí)候如果業(yè)務(wù)邏輯發(fā)生某些改變,那就意味著你的三層Model以及對(duì)應(yīng)的Mapper都需要去更改,這樣簡(jiǎn)直不可接受,改動(dòng)量太大,并且有的時(shí)候各層之間的Model幾乎是一樣的,這意味著古板的復(fù)制黏貼代碼,不符合DRY Principle(Dont Repeat Yourself)。所以按照zhengxiaopeng的意見(jiàn)來(lái)說(shuō),去除Presentation層的UserModel,只在Domain層和Data層保留相關(guān)代碼,這樣實(shí)際上沒(méi)有破壞依賴規(guī)則。Presentation層可以獲取Domain層對(duì)象的引用,在Presenter通過(guò)Mapper轉(zhuǎn)換,將正確的對(duì)象提供給View去展示。我認(rèn)為這是一個(gè)比較好的思路。相對(duì)來(lái)說(shuō),每層都定義一個(gè)model顯得過(guò)于古板,對(duì)于大型程序來(lái)說(shuō)代價(jià)也十分昂貴。
關(guān)于Stay(校長(zhǎng)!)的看法:這算是一個(gè)必要的冗余,每一層間的數(shù)據(jù)傳遞都有對(duì)象的豐富與隱藏,用不同的object來(lái)指代更容易解耦。更具體的來(lái)說(shuō)主要是因?yàn)槭謾C(jī)端的use case基本上都是crud,太簡(jiǎn)單了,domain層沒(méi)有發(fā)揮太大的作用,而如果這一層只是作為接頭的中間層,是可以無(wú)限弱化,甚至刪掉這一層的。也就跟p層合并了。
最重要的是:復(fù)雜的設(shè)計(jì)可以通過(guò)增加中間層來(lái)簡(jiǎn)化,反過(guò)來(lái)一樣,如果設(shè)計(jì)很簡(jiǎn)單,那壓根就不需要中間層。自己要掌握這個(gè)度。
結(jié)尾:
Architecture is about intent, we have made it about frameworks and details。架構(gòu)的核心在于目的,具體的框架、細(xì)節(jié),要根據(jù)我們實(shí)際項(xiàng)目,實(shí)際的需求,做具體的實(shí)現(xiàn)。每個(gè)人對(duì)架構(gòu)都有著不同的看法,以及具體的實(shí)施細(xì)節(jié)。但是當(dāng)你使用Clean架構(gòu)做為項(xiàng)目主架構(gòu)的時(shí)候,請(qǐng)務(wù)必牢記洋蔥圖的依賴規(guī)則,以及各層之間的跨域規(guī)則,這將讓你減少很多煩惱。最后用一張UML圖結(jié)束這篇文章。
關(guān)于業(yè)務(wù)邏輯是什么你可以參考這個(gè)(我相信大部分人不知道):
http://www.uml.org.cn/zjjs/201008021.asp
關(guān)于如何向Use Case傳遞動(dòng)態(tài)參數(shù):
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
架構(gòu)宏觀上的參考:
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
關(guān)于依賴關(guān)系的參考:
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