如何從MVP模式進階到Clean模式
從類圖上來看,MVP都是一個業務一個Presenter,每個Presenter都是一個接口,它還包含了View的接口,用於定於和View相關的行為,然後Activity等業務類實現View的接口,因為UI有關的操作只能在UI線程。
采用MVP模式,和View相關的接口都要由業務類實現,自然,業務類本身就會有數量不小的方法,而邏輯相關的接口可以放在Presenter裏面,然後由一個PresenterImpl去實現。
說實話,雖然這樣看上去確實將Activity變成各種View和Presenter組合的模塊,但是控件的聲明還是在Activity,所以Activity的職責就變成了View的方法實現和控件聲明。
如果我們只是單純的想要在MVP模式下輸出一句Hello World到TextView上,是一件不太容易的事情,當然,MVP本身就不是用來解決這種微小場景。
是否有更加簡單的模式能夠應付所有場景?答案是幾乎沒有的,都是要結合實際場景,在大的框架模式不變的情況下,允許做小的調整,比如一個應用整體的框架是MVP模式,但一些實在是細微的場景,有必要也使用嗎?
MVP模式並不是插拔式的,雖然說真正在執行邏輯處理的是Presenter,而Activity只是提供對應的方法實現,但是假設要移除這塊業務,要清理的東西也是很多的,但是它可以實現一定程度的復用,只要實現了View的接口,通過對應的Presenter,就可以擁有這些接口定義的行為協議,但是也僅僅只是行為協議,具體的實現還是要自己編寫。
這所謂的復用,就是將業務類作為一個系統運作的部件,只要能夠滿足部件的要求,系統就能馬上運轉起來。
MVP為了保證業務類更好的成為部件,它就必須盡可能的解耦,如和UI相關的接口在View接口裏,而邏輯相關的方法在Presenter裏,並且還會多一層PresenterImpl,每個View的接口方法在Presenter哪個方法裏面調用,都是在這裏進行處理,從而將業務類打造成一個清晰的部件,它和其他部件之間沒有任何關系。
當然,我們也可以將業務類上升為PresenterImpl,這樣就能減少很多類的編寫,不過並不建議這樣做,因為業務類如果成為PresenterImpl,那麽如果出現類似功能的業務,就需要到這個業務類復制粘貼相關的代碼,如果將這些邏輯鎖在一個PresenterImpl裏,只要聲明這個PresenterImpl就能使用了。
每個業務就算相似,也可能存在一定的差異,為了保證結構通用,是要盡量避免引入這些差異,差異就要隔離在對應的類裏面,這就導致可能會有一些AbstractPresenter的產生,然後每個業務的Presenter在繼承AbstractPresenter的通用行為基礎上,再實現自己的行為。
這是不可避免的,如果真的想要結構上清晰,提取通用業務是一定要的,而這部分工作往往會帶來類數量的增加,因為它只是通用類,具體業務也會有自己對應的業務類。
我們可以這麽理解,MVP就是合理的規定,哪部分代碼應該寫在那裏,而誰應該寫哪部分代碼的一個約定,這也是MVC,MVVM等框架模式都具備的能力。
在我們以往認知中,依賴倒置原則要求我們面向抽象編程,每個部分都應該是和抽象打交道,而不是具體的實現,實現是會變的,但是抽象是幾乎不會變的。
只要好好遵循這個原則,都能寫出很好的代碼,結構清晰,並且具有良好的維護性,無論MVP,MVC,還是MVVM,本質都是這個原則的實現產物。
MVP相比MVC來說,它有一個優勢:它的Presenter是可以測試的。
單純的Presenter是沒有和View有任何關聯的,有關聯的是PresenterImpl,所以我們如果只是測試Presenter,是並不牽扯到UI,而Android的測試中,涉及到UI都是很麻煩的事情。
對Presenter的測試,就和傳統的接口測試是一樣的。
對於Android開發人員來說,由於xml布局文件的存在,View的繪制並不只是單純的代碼編寫,我們希望布局也能夠復用。
Android為布局的復用提供了一定的支持,像是include的使用,就能把一個布局,切割成幾個布局的組合。
我們可以把每個布局按照本身一定的功能,劃分為幾個include,然後每個include都有自己的邏輯處理類,比如說,我們可以定義一個ViewController,這個ViewController負責View的初始化和對應功能實現,那麽一個Activity本身就只是多個ViewController的組合類,而Activity的布局,也是多個include的組合。
如果劃分得足夠清楚,可以通過組合不同的ViewController來實現不同的界面組合。
這種模式在定義上,和MVC是一致的,ViewController就是Controller,Android的Activity本身在Android的機制裏面其實也是Controller,只不過對於開發者來說,接觸到的是Activity,所以就在Activity上做功夫了。
在我們以往的依賴關系或者結構設計上,都是類似樹狀的結構,通過賦予每個類依賴,實現或者繼承的關系,將他們連接到一起。
這種結構就是依賴關系樹。
之所以是依賴關系樹這種形狀,很大關系是因為我們的設計,實質上只有兩種:自頂而下或者自下而上,我們會先確定一個點,然後從這個點開始延伸出和其他點的關系,從而不斷輻射出去。
Clean模式在依賴關系上,是畫一個同心圓。
Clean模式的解釋是,依賴應該是從外到內,因此在實現上,率先實現內層,再實現外層,而每一層都會把內部那一層完全包裹起來,形成一個類似洋蔥的結構。
我們在實現上,通常都會實現最核心的那一層,比如說,我們想實現一個功能,輸出Hello World,那麽首先要實現的就是輸出Hello World的方法,不過我們這個方法後面可能不只是輸出Hello World,因此就抽象成輸出傳入的String的方法,然後我們再編寫外部傳入String的方法,有可能是鍵盤輸入,有可能是其他輸入等等。。。這樣一層一層寫下去,一直寫到觸發這個需求的地方,需求的最初位置,也是依賴的起點。
所以我們平常的做法和Clean模式對於依賴的理解是沒啥區別的,當然,也不能說Clean模式就是畫個圓就說是新的東西,它本身更加強調的是,幹凈。
Clean,就是幹凈,而什麽樣的程度是幹凈,幹凈又能做到什麽呢?
所謂的幹凈,是因為Clean模式的依賴是從外到內,因此內部對外部是無感知的,就像我們剝洋蔥,每剝掉一層,裏面依然還是完整的,只不過變小的洋蔥。
Clean模式是如何做到這樣的獨立性呢?
在Clean模式中,DB,Web,Devcices等數據來源是最外層,這個沒毛病,數據輸入都是任何一個系統的起點,但是它把UI也放在了最外層。然後再進一步的層級是Controllers,GateWays和Presenters等,按照依賴從外到內的設計思想,這些層級也是最先接觸這些數據來源和UI,接著的層級就是Use Cases,也就是用戶場景,最裏面就是Entities,這個可以理解為用戶場景中的實體。
UI變成了一個獨立的層級,和DB,Web等並列,而DB,Web這些層級在設計上,原本就具備自測的能力,並且它應該也是最先被測試的,因為它們是框架提供的能力。這樣一層層下去,每個內層在測試的時候,只需要了解它的內層,不知道它的外層。
我們好奇的是,UI怎麽就變成了一個獨立的層級?
這裏的UI應該是各種組件,前面有關MVP的討論中也提到,PresenterImpl確實是需要從外部傳入View接口的實現類,所以UI作為最外層的依賴,也是沒問題的。
在Android中,一般的層級並不會超過三層。實現層,也就是框架層,像是Web,DB等,就是Clean模式的外層,而中間層是接口適配層,負責連接實現層和內層的業務邏輯層。
在Clean模式中,業務邏輯層,也就是內層,應該是對外層毫無感知的,所以我們測試業務邏輯層,完全可以跳過框架層。
我們很常見的做法就是在Activity中調用網絡接口來獲取數據,然後在對應的回調中將該數據展示到對應的控件上,如果按照Clean模式,這時候不應該是直接就將數據和控件進行綁定,中間要有一個接口適配層,將這兩者獨立開來。
任何時候,兩個獨立的部分都不應該直接交互,而是要通過一個抽象,依賴倒置原則在這裏的產物就是接口適配層。
對於Android,Clean模式的要求就是業務邏輯層完全不能持有外層的引用,也就是說,內層提供一個它需要的數據模型,而接口適配層負責將框架層傳遞過來的數據轉化為業務邏輯層需要的數據模型,然後再傳給業務邏輯層,這就是適配層的工作。
因此,在Android中,Clean模式的內層必須暴露接口,以便外部傳遞需要的數據,而外層是知道這些數據模型,可以跳過內層,組裝這些數據模型進行測試。
無法應用到實際場景的模式都是假模式,因此我們現在趕緊開始試試Clean模式在代碼上是如何表現的。
我們可以簡單點,假設一個用戶場景:獲取到某個來源的字符串,然後顯示到TextView上。
從最核心的內層開始編寫。
最核心的內層就是用戶場景,並且它是與外層毫無關聯的,但是它需要暴露一個回調,負責和上層打交道,所以它接受一個字符串,然後輸出這個字符串:
public class Interactor { private Callback mCallback; public Interactor(Callback callback) { mCallback = callback; } public void run(String info) { if (mCallback != null) { mCallback.showInput(info); } } public interface Callback { void showInput(String info); } }
對於MVP模式來說,最核心的部分就是Interactor,它負責將外部的Model轉換成ViewModel。
在MVP中,Presenter是不直接操作View和Model的,它要做的工作就是把ViewModel傳給View。Model並不等於ViewModel,Model是原始的數據,而ViewModel是視圖數據,比如說一個登陸頁面上的密碼和用戶名,這些數據的集合就是一個ViewModel。
Interactor既然作為最內層,它就不應該直接和最外層拿Model,所以我們需要一個中間層Converter。
我們假設這裏的場景是從User模型中獲取name字段,這就是Interactor的數據來源,那麽Converter的職責就是從接受到的User模型中取出name傳給Interactor。
public class Converter {
public void converterNameToInteractor(Callback callback, User user) {
new Interactor(callback).run(user.getName());
}
}
依賴是從外到內的,所以Interactor不能直接從外層設置Callback,它不能和外層有任何交集,這部分工作都在中間層Converter。
Clean模式向我們做出了保證,Interactor是可以測試的,而且應該是可以獨立於Android系統,在任何JVM機器上測試的代碼,因為它已經隔離了外層的依賴,而這部分隔離的標準就是Interactor不依賴於任何框架的庫和包,它依賴的是Java的庫和包。
按照Clean模式的原則,我們這裏的Converter有個問題:它知道了User這個Model。
這個有什麽問題呢?User是外層的數據模型,而Converter從這個Model提取出Interactor需要的name這個String,然後Interactor通過Callback將name這個ViewModle傳遞給外層UI顯示。
這個過程完全沒有問題,但是Clean模式要求內層不能知道外層的東西,中間層的Converter現在知道了外層的User,這個是要剝離的。
我們只要將User這個參數修改為String就可以了。
這樣就會產生一個疑問:Converter不是將外層的數據模型轉換為內層的數據模型嗎?那麽User.getName這個操作放在Converter不是應該的嗎?
道理上是這樣的沒錯,但是為了保證依賴上的幹凈,比如說,我們現在要測試Converter,但是有了User這個依賴,就要知道外層了,就不能獨立測試了,而且我們這裏只是一個簡單的參數,假設我們的Interactor需要的是不同類型的多個字段,它自己本身可能會為此提供一個Model,那麽Converter的職責就是接受需要的字段,然後組裝這個Model。
為了獨立,犧牲了便利,這在代碼設計上是很常見的考慮,但是我們也要權衡一下,這犧牲的便利和換來的獨立,哪個更加重要。
接下來我們只要讓Activity實現Callback接口就可以了。
public class MainActivity extends AppCompatActivity implements Interactor.Callback{
private TextView textView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
textView = (TextView) findViewById(R.id.text);
Converter converter = new Converter();
User user = new User("張三");
converter.converterNameToInteractor(this, user.getName());
}
@Override
public void showInput(String info) {
textView.setText(info);
}
}
Activity就是外層,它這裏有數據的來源,UI的展示。
Clean模式大概就是這樣的結構,我們可以看到依賴確實是一步一步傳遞過去的,每一層都可以獨立於外層進行測試。
這一路下來,Presenter去了哪裏?
當然,這裏我們還是可以抽取出Presenter:
public class Presenter implements Interactor.Callback{ private View mView; public Presenter(View view){ this.mView = view; } @Override public void showInput(String info) { if(mView != null) { mView.showText(info); } } public interface View{ void showText(String text); } }
然後Activity再修改成這樣:
public class MainActivity extends AppCompatActivity implements Presenter.View{ private TextView textView; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); textView = (TextView) findViewById(R.id.text); Presenter presenter = new Presenter(this); presenter.showInput(new User("張三").getName()); } @Override public void showText(String text) { textView.setText(text); } }
實際上,Converter和Presenter是並列的,前面的例子中,Converter實際上是把Presenter的工作也做了,不是一個單純的轉換,是有一個分配數據的操作,所以我們如果要加入Converter這個層級,必須保證它就僅僅只是一個數據轉換的類。
現在我們修改Converter:
public class Converter { public String converterName(String name) { return name; } }
然後Activity再這樣修改:
public class MainActivity extends AppCompatActivity implements Presenter.View{ private TextView textView; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); textView = (TextView) findViewById(R.id.text); Presenter presenter = new Presenter(this); Converter converter = new Converter(); User user = new User("張三"); presenter.showInput(converter.converterName(user.getName())); } @Override public void showText(String text) { textView.setText(text); } }
現在只是因為Converter要轉換的類型只是String的單一字段,如果是自定義類型,就不會顯得這麽小題大做了。
Clean模式的依賴是從外到內,那麽外層能否跳過中間層,直接接觸內層呢?比如說,我們這裏的Activity幹脆就實現Callback這個接口?
最好不要這樣做,我們要保證幹凈,就要徹底的隔離,保證每剝掉一層,內部還是完整的。
不過基於Java的語言特性,內層提供的接口,誰都可以實現,然後只要由中間層傳過來就行了,也是能夠保證依賴從外到內的原則,如果真的想要嚴格隔離,我們這裏利用訪問權限來控制,將中間層和內層放在同一個包裏,內層的接口都只有包訪問權限,不同包的外層自然就無法訪問到了。
命名空間在隔離上是能夠發揮特別好的作用,因此我們要做好包的管理。
在很多實際的編碼工作,如果嚴格按照外層-->中間層-->內層,內層-->中間層-->外層這種訪問順序,可以預計,中間層的類的數量可能會爆炸,比如內層不能直接訪問數據庫這樣的外層,那麽它就會通過一個中間層來獲取數據,有可能只是簡單的查詢。
當然,我們可以優化中間層,比如對某個表的操作可以合並到一個類裏面,這樣就會減少很多中間層。
嚴格並不是壞事,規矩還是要遵守的,我們能做的靈活,就是中間層的管理,但是外層和內層,是必須保證一定是隔離開來的。
如何從MVP模式進階到Clean模式