自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

一次高效簡單地組織代碼—簡單的 ViewModel 實踐

移動開發(fā)
不知不覺,筆者也擼碼也已經(jīng)一年多了。隨著擼碼的數(shù)量疾速上漲,如何高效,簡單的組織代碼,經(jīng)常引起筆者的思考。作為一個方法論及其實踐者(這個定義是筆者自己胡謅的),始終希望能夠找到一些簡單、有效的方法來解決問題,由此,也開始了一段構(gòu)建代碼的實踐體驗。

[[140264]]

前言

不知不覺,筆者也擼碼也已經(jīng)一年多了。隨著擼碼的數(shù)量疾速上漲,如何高效,簡單的組織代碼,經(jīng)常引起筆者的思考。作為一個方法論及其實踐者(這個定義是筆者自己胡謅的),始終希望能夠找到一些簡單、有效的方法來解決問題,由此,也開始了一段構(gòu)建代碼的實踐體驗。

這次要分享的,是自己在長期實踐 MVVM 結(jié)構(gòu)后,對 MVVM 框架的一些理解與自己的工作流程。其中或許還有一些地方拿捏欠妥,希望大家能一起相互交流。

前戲

ViewModel 這個概念是基于 MVVM 結(jié)構(gòu)提出的,全稱應(yīng)該叫做 Model-View-ViewModel,從結(jié)構(gòu)上來說,應(yīng)該是 Model-ViewModel-ViewController-View。簡單來說,就是在 MVC 結(jié)構(gòu)的基礎(chǔ)上,將 ViewController 中數(shù)據(jù)相關(guān)的職能剝離出來,單獨形成一個結(jié)構(gòu)層級。

關(guān)于 ViewModel 的詳細(xì)定義,可以參考這篇 MVVM介紹。

此外,在工作流中,筆者在一定程度上參考了 BDD 的代碼構(gòu)建思路,雖然沒有真正意義上的按照行為構(gòu)建測試代碼,但是其書寫過程與 BDD 確實有相似之處。關(guān)于 BDD,可以參考這篇 行為驅(qū)動測試。

為本篇文章所編寫的 Demo 已經(jīng)傳至 Github:傳送門~

好吧,我們開始。

ViewModel 與 ViewController

基類

嗯,在這里,需要用到 OOP 的經(jīng)典模式 —— 繼承。

我們不打算把 ViewModel 的功能構(gòu)建的太重,所以,它只需要一個指向擁有自己的 ViewController 指針,與一個賦值 ViewController 的工廠方法。 就像下面這段代碼:

  1. //BCBaseViewModel.h 
  2.  
  3. @interface BCBaseViewModel : NSObject 
  4.  
  5. @property (nonatomic,weak,readonly) UIViewController *viewController; 
  6.  
  7. +(BCBaseViewModel *)modelWithViewController:(UIViewController *)viewController; 
  8.  
  9. @end 

ViewModel 只需要一個 weak 類型的 viewController 指針指向自己的 viewController,而 viewModel 則由 viewController 使用 strong 指針持有,用于規(guī)避循環(huán)引用。

這樣,就足夠了。

委托者與代理者

為了讓 ViewModel 與 ViewController 的關(guān)系更加清晰,也為了能夠批量化的生產(chǎn) ViewModel,接下來要定義的,就是 ViewModel 與 ViewController 的結(jié)構(gòu)特征了。

在分析了 ViewModel 劃分層次的原因與主要承擔(dān)的功能之后,我們大致可以總結(jié)出這么幾個特征:

ViewModel 與 ViewController 是一一對應(yīng)的

ViewModel 實現(xiàn)的功能是從 ViewController 中剝離出來的

ViewModel 是 ViewController 的附屬對象

根據(jù)上面幾點特征,最容易想到的類間關(guān)系應(yīng)該就是代理/委托關(guān)系了,把一眼就看到的關(guān)系說的復(fù)雜可能會招罵,但是對接下來的論述,上面多多少少會起到點決定性的作用。

比如,雖然確定了代理與委托,但究竟誰是代理者,誰是委托者呢?換句話說,誰是協(xié)議的制定方,而誰又是實現(xiàn)方呢?

筆者這里給出兩個依據(jù)來確認(rèn)。

協(xié)議方法是被動調(diào)用方法,也就是反向調(diào)用?;诖耍瑓f(xié)議的實現(xiàn)方,應(yīng)該同時是事件的響應(yīng)方,以事件驅(qū)動正向調(diào)用,再由此觸發(fā)反向調(diào)用。

協(xié)議的實現(xiàn)方實現(xiàn)的方法是通行,且抽象的。反推之,協(xié)議的制定方需要實現(xiàn)更難抽象或是更為具體的方法。 這個依據(jù)也可以從另外一個層面來理解,即協(xié)議的實現(xiàn)方的可替換性應(yīng)該更強(qiáng)。

第一條依據(jù)相對毋庸置疑,畢竟 ViewController 是 View 的持有者與管理者,更是 View 與 ViewModel 相互影響的唯一渠道。讓 ViewModel 作為 View 事件的響應(yīng)方來驅(qū)動 UIViewController,從結(jié)構(gòu)上有些說不通。

第二條則是實踐得來的結(jié)論,在實際開發(fā)時,由外而內(nèi),視圖的修改頻度往往是大于數(shù)據(jù)的。因此,重構(gòu) ViewController 的概率也要大于重構(gòu) ViewModel 的概率。不過這種歸納性的結(jié)論無法一言蔽之,反而會建議諸位在實際的開發(fā)過程當(dāng)中,應(yīng)當(dāng)針對這些開發(fā)訴求對結(jié)構(gòu)做更靈活的調(diào)整和優(yōu)化。

這次實踐,則會以 ViewModel 作為協(xié)議的制定方,來構(gòu)建代碼。

讓協(xié)議輕一點

在 OC 中,有 @protocol 相關(guān)的一系列語法專門用于聲明與實現(xiàn)協(xié)議相關(guān)的所有功能。但是考慮到具體的 ViewModel 與 ViewController 之間的相互調(diào)用都各不相同,如果我們?yōu)槊恳唤M ViewModel 與 ViewController 都聲明一份協(xié)議,并且交由彼此實現(xiàn)和調(diào)用,代碼量激增基本上是一種必然了。

為了讓整個協(xié)議結(jié)構(gòu)輕一點,這里并沒有采用 @protocol 相關(guān)語法來實現(xiàn),而是利用如下代碼:

  1. typedef NSUInteger BCViewControllerCallBackAction; 
  2.  
  3. @interface UIViewController(ViewModel) 
  4.  
  5. -(void)callBackAction:(BCViewControllerCallBackAction)action info:(id)info; 
  6.  
  7. @end 

這段代碼做了這么幾件事:

利用分類,為 UIViewController 拓展了 ViewModel 相關(guān)的回調(diào)方法聲明。功能類似于父類聲明抽象接口,而交由子類去實現(xiàn)。

接口支持傳參,具體的類不再制定協(xié)議方法,而只需要協(xié)議參數(shù)。

將該分類聲明在 ViewModel 的基類中,即可保證對 ViewModel 可見的 UIViewController 都實現(xiàn)了協(xié)議方法,從而不需要再編寫 @protocol 段落。

在具體的 ViewModel 與 ViewController 子類中,只需要根據(jù)具體的需求設(shè)計回調(diào)參數(shù),構(gòu)建一個 對應(yīng)的枚舉即可。

將整個協(xié)議結(jié)構(gòu)輕質(zhì)化,主要的原因是因為協(xié)議內(nèi)容變動頻繁。使用枚舉而非 protocol,可以減小改動范圍,且代碼量較少,定制方便。

筆者曾經(jīng)也嘗試過雙向抽象方法定義,即對 ViewModel 也做一些抽象方法,使雙方僅根據(jù)基類約定的協(xié)議工作。但實踐下來,ViewModel 的方法并不易于抽象,因為其公共方法往往直接體現(xiàn)了 ViewController 的數(shù)據(jù)需求。如果強(qiáng)行擬訂抽象方法,反而會在構(gòu)建具體類時產(chǎn)生歸納困惑,由此產(chǎn)生的最壞結(jié)果就是放棄遵守協(xié)議,整個代碼反而會變的難以維護(hù)。

化需求為行為

在開發(fā)過程當(dāng)中,最常見的開發(fā)流還是需求驅(qū)動型開發(fā)流。說白了,就是扔給你一張示意圖,有時運氣好點還有交互原型神馬的(運氣不好就是別人家的 App = =),然后就交由你任性的東一榔頭西一棒槌的寫寫畫畫。

這個時候,還是建議適當(dāng)?shù)囊?guī)劃一下開發(fā)流程。主要是考慮這么幾點:

開發(fā)層級與順序;

單位時間內(nèi)只關(guān)心盡可能少的東西;

易于構(gòu)建和調(diào)試;

合理簡省重復(fù)性工作。

其實說簡單點,就是讓整個工作流變的有規(guī)則和秩序,以確保開發(fā)有理有據(jù)且可控。另外,也能有效避免反錯的頻率和嚴(yán)重程度。

這里,筆者不要臉的分享自己的簡易工作流。

整個過程并不復(fù)雜,其實就是先擼 ViewController 界面,遇到需要數(shù)據(jù)的地方,就在 ViewModel 中聲明一個方法,然后佯裝調(diào)用。擼的代碼大概是這個樣子的:

  1. typedef NS_ENUM(BCViewControllerAction,BCTopViewCallBackAction){ 
  2. BCTopViewCallBackActionReloadTable = 1 < < 0
  3. BCTopViewCallBackActionReloadResult = 1 << 1 
  4. }; 
  5.  
  6. @interface BCTopViewModel : BCBaseViewModel 
  7.  
  8. - (NSString *)LEDString; 
  9.  
  10. - (NSUInteger)operationCount; 
  11.  
  12. - (NSString *)operationTextAtIndex:(NSUInteger)index; 
  13.  
  14. - (void)undo; 
  15.  
  16. - (void)clear; 
  17.  
  18. @end 
  19.  
  20. @interface BCTopViewController ()@property (nonatomic,strong) BCTopViewModel *model; 
  21. @property (nonatomic,weak) IBOutlet UITableView *operationTable; 
  22. @property (nonatomic,weak) IBOutlet UILabel *result; 
  23. @end 
  24.  
  25.  
  26. @implementation BCTopViewController 
  27.  
  28. - (void)viewDidLoad{ 
  29. [super viewDidLoad]; 
  30. self.operationTable.tableFooterView = UIView.new
  31.  
  32.  
  33. #pragma mark - action 
  34.  
  35. - (IBAction)undo:(UIButton *)sender{ 
  36. [self.model undo]; 
  37.  
  38. - (IBAction)clear:(UIButton *)sender{ 
  39. [self.model clear]; 
  40.  
  41. #pragma mark - call back 
  42.  
  43. - (void)callBackAction:(BCViewControllerAction)action{ 
  44. if (action & BCTopViewCallBackActionReloadTable) { 
  45. [self.operationTable reloadData]; 
  46. if (action & BCTopViewCallBackActionReloadResult) { 
  47. self.result.text = self.model.LEDString; 
  48.  
  49. #pragma mark - tableView datasource & delegate 
  50.  
  51. - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section{ 
  52. return self.model.operationCount; 
  53.  
  54. - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{ 
  55. UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cell" forIndexPath:indexPath]; 
  56. cell.textLabel.text = [self.model operationTextAtIndex:indexPath.row]; 
  57. return cell; 
  58.  
  59. @end 

這樣開發(fā)利用了一個 Runtime trick,那就是 Nil 可以響應(yīng)任何消息。

所以,雖然我們只聲明了方法,并沒有實現(xiàn),上面的代碼也是隨時可以運行的。換言之,你可以隨時運行來調(diào)試界面,而不用擔(dān)心 ViewModel 的實現(xiàn)。

相對麻煩的是測試回調(diào)方法,筆者自己的建議是在編寫好回調(diào)方法之后,在 ViewController 中對應(yīng)的 ViewModel正向調(diào)用之后直接調(diào)用自己的回調(diào),如果遇到可能的網(wǎng)絡(luò)請求或者需要延時處理的回調(diào),也可以考慮編寫一個基于 dispatch_after 的測試宏來測試回調(diào)。

一般來說,視圖界面層的開發(fā)總是所見即所得的,所以測試標(biāo)準(zhǔn)就是頁面需求本身。當(dāng)肉眼可見的所有需求實現(xiàn),我們的界面編寫也就告一段落了。當(dāng)然了,此時的代碼依舊是脆弱的,因為我們只做了正向?qū)崿F(xiàn),還沒有做邊界用例測試,所以并不知道在非正常情況下,是否會出現(xiàn)什么詭異的事情。

不過值得慶幸的是,我們已經(jīng)成功的把 ViewController 中數(shù)據(jù)相關(guān)的部分成功的隔離了出去。在未來的測試中,發(fā)現(xiàn)的任何與數(shù)據(jù)相關(guān)的 BUG,我們都可以拍著胸脯說,它肯定和 ViewController 無關(guān)。

另外,一如我所說,需求本身就是頁面的測試標(biāo)準(zhǔn)。也就是說,當(dāng)你實現(xiàn)了需求,你的視圖層就已經(jīng)通過了測試。是的,我要開始套用 TDD 的思考方式了。我們已經(jīng)拿著需求當(dāng)了測試用例,并且一一 Pass。

而當(dāng)我們開發(fā)完 ViewController 的同時,我們也已經(jīng)為 ViewModel 聲明好了所有公共方法,并且在對應(yīng)的位置做了調(diào)用。BDD 的要點在于 It...when...should 的行為斷言,在此時的環(huán)境下,It 就是 ViewModel,when 就是 ViewController 中的每次調(diào)用,而 should ,則對應(yīng)著 ViewModel 所有數(shù)據(jù)接口所衍生出的變化。

換句話說,我們可能沒辦法從界面上看到所有的行為引發(fā)的變化,但是我們已經(jīng)在 ViewModel 實現(xiàn)之前構(gòu)建了一個可測試環(huán)境。如果時間充足的話,此時的第一件事應(yīng)當(dāng)是根據(jù)具體的調(diào)用環(huán)境,為每個公共方法編寫足夠強(qiáng)壯的測試代碼,來避免數(shù)據(jù)錯誤。

順便說幾句風(fēng)月場上的虛話。在構(gòu)建程序的時候,面向接口是優(yōu)于面向?qū)崿F(xiàn)的,因為在任何一個系統(tǒng)中,比起信息的產(chǎn)生,信息的傳遞更決定著系統(tǒng)本身是否強(qiáng)大。而編寫代碼的時候,先將抽象功能方法具體化,再將數(shù)據(jù)逐步抽象化,經(jīng)歷一個類似梭型的過程,可以更完美的貼合“高內(nèi)聚、低耦合”的目標(biāo)。

Fat Model

如果單從 ViewModel 實踐上來說,以上的內(nèi)容已然解釋的差不多了。不過鑒于筆者手賤擼了一整個 Demo,就額外解釋下其它幾個地方的設(shè)計了。

首先是關(guān)于胖 Model 的設(shè)計。關(guān)于胖瘦 Model 的概念筆者也是最近才從這篇 iOS 應(yīng)用架構(gòu)談 view層的組織和調(diào)用方案 上看到。在此之前,只是憑直覺和朋友討論過 Model 與 Model 之間也應(yīng)該有所區(qū)分。

Model 的胖瘦是根據(jù)業(yè)務(wù)相關(guān)性來劃分的。所以,筆者有時會直接將胖 Model 稱之為業(yè)務(wù)層 Model 以區(qū)分瘦 Model。在示例代碼中,CalculatorBrain 應(yīng)該算是一個相對標(biāo)準(zhǔn)的業(yè)務(wù)層 Model 了。

如果遇到單個 ViewModel(或者 MVC 中的 Controller)無法解決的需求時,就需要整體業(yè)務(wù)下沉,交給一個相對獨立的 Model 來解決問題。上層只持有該 Model 開放出來的接口,以此促成的業(yè)務(wù)層 Model,帶有明顯的業(yè)務(wù)痕跡,說白了,就是不容易復(fù)用。

不過,筆者自己的開發(fā)觀點是,弱業(yè)務(wù)相關(guān)的模塊復(fù)用性應(yīng)該強(qiáng),即功能應(yīng)該盡量單元化。而強(qiáng)業(yè)務(wù)相關(guān)的模塊則應(yīng)該有更好的重構(gòu)性和替換性能,即盡可能的功能內(nèi)聚。說簡單點,比如這個 Demo 不再是一個計算器,而需要變成一個計數(shù)器或者別的什么,需要重構(gòu)的就只有 CalculatorBrain 這個類。(當(dāng)然,這只是基于假設(shè),界面不變底層數(shù)據(jù)狂變的需求不敢想象…)

從另外一方面來看,在整個 MVVM 框架中,也可以將每個單獨的 ViewModel 視作一個管道。在整個業(yè)務(wù)鏈中做了雙向的抽象,使整個業(yè)務(wù)鏈各個部分的替換性都有所提升,筆者個人傾向于將其解釋為,通過設(shè)計中間層,均衡了上下層級的復(fù)雜度。

更輕量級的 ViewController

objccn.io 第一期的第一篇文章就是更輕量的 View Controllers。文章內(nèi)曾提到,通過將各個 protocol 的實現(xiàn)挪到 ViewController 之外,來為 ViewController 瘦身。

筆者也曾是這個建議的實踐者之一,甚至一度認(rèn)為這也是 ViewModel 的主要功能。不過隨著開發(fā)時間拉長,筆者不得不重新開始審視這個問題。

首先,這種方法會產(chǎn)生很多額外的接口。我們依舊用 UITableView 來舉例。 假設(shè)我們讓 ViewModel 實現(xiàn)了 UITableViewDelegate 與 UITableViewDataSource 協(xié)議。這個時候,如果 ViewController 的另一個控件想要根據(jù) tableView 的滾動位置做出響應(yīng)該怎么辦呢?由于 ViewModel 才是 tableView 的 delegate,所以我們就需要為 ViewController 聲明額外的公共方法,供 ViewModel 在回調(diào)方法中調(diào)用。

而我們不難發(fā)現(xiàn),基本所有視圖控件的 Delegate 協(xié)議都涉及到視圖本身的響應(yīng),只要涉及到同界面下不同控件元素的互動,就不可避免的需要 ViewController 的參與。

筆者也嘗試過將 UITableViewDelegate 實現(xiàn)在 ViewController 中,而把 UITableViewDataSource 托付給 ViewModel 的方式。蛋疼的事情發(fā)生在動態(tài)高度 Cell 的實現(xiàn)上,我們一方面在 ViewModel 內(nèi)部給 tableView:cellForRowAtIndexPath 輸入數(shù)據(jù),一方面卻又要為 tableView:heightForRowAtIndexPath: 開設(shè)接口提供相同的數(shù)據(jù)以供計算高度。

筆者最后總結(jié)了原因,是因為 View 層 與 ViewController 層本身是持有與被持有的依賴關(guān)系,所以任何類作為 ViewController 的類內(nèi)實例來實現(xiàn)協(xié)議回調(diào),實際上都是在跨層調(diào)用,所以,就注定要以額外的接口為代價,換言之,ViewController 的內(nèi)聚性變差了。

而另外一方面的原因,則是關(guān)于測試。我們說 ViewController 難以測試的原因是因為在大部分情況下,它并沒有幾個像樣的公共方法,且私有方法中還有一大部分方法是傳參回調(diào)。如果我們將這些 protocol 實現(xiàn)在另一個類中,其實并不會提升它們的可測試性。更為行之有效的方式,應(yīng)該是將 protocol 的實現(xiàn)與數(shù)據(jù)接口隔離開來,讓實現(xiàn)方通過接口來填充數(shù)據(jù),而非自身。

在 Demo 中,TopViewModel 便為 cell 的內(nèi)容填充開設(shè)了 operationCount 與 operationTextAtIndex: 這樣的數(shù)據(jù)接口。相信,為這樣的數(shù)據(jù)接口構(gòu)造測試環(huán)境,要比為 tableView:cellForRowAtIndexPath 這種方法構(gòu)造測試環(huán)境要簡單的多。從側(cè)面來看,這樣的接口反而更合適于測試覆蓋。

基于以上這兩點原因,在之后的開發(fā)中,筆者開始將越來越多的 protocol 又請回了 ViewController 中。并且,由于 ViewModel 的存在,筆者更傾向與將 ViewController 構(gòu)建成為一個獨立實現(xiàn)且只負(fù)責(zé)實現(xiàn)界面布局、邏輯的類,讓一個類做更少的事,但做的更好。

后記

本文的相關(guān) Demo,實現(xiàn)的功能并不復(fù)雜,甚至有些簡陋的不好見人。見責(zé)于筆者想象力不周,本著以實踐演示為主的心態(tài),做個參考就好吧。

筆者自詡為方法論及其實踐者,比較認(rèn)同“構(gòu)建代碼的方法比代碼更有價值”這個觀點。寫出一兩句驚艷的代碼或許是運氣,掌握方法去構(gòu)建代碼本身才是戰(zhàn)斗力吧。盡可能讓自己每一句代碼都有理有據(jù),而不是隨心所欲,也覺得會比較負(fù)責(zé),起碼寫起來有個交待。

以上的總結(jié)見識有限,很多地方或許會有疏漏之處,希望能與諸位看官一起交流,如果能指出其中疏漏甚至錯誤的觀點,那就不甚感激了。

另外,說點說出來就不嫌丟人的話。截至筆者寫完這篇博文,雖然對“設(shè)計模式”的相關(guān)概念有各種旁敲側(cè)擊的求證與查詢,但仍未系統(tǒng)學(xué)習(xí)過相關(guān)概念。說來慚愧,有時候自己花好大功夫才弄明白、想清楚的答案,突然發(fā)現(xiàn)某本書、某篇文章上早已幾句話講的明明白白,其實還挺挫敗的。次數(shù)多了,甚至?xí)ξ粗闹R產(chǎn)生抗拒,用來安慰自己很牛逼,這也是特地聲明沒有系統(tǒng)學(xué)習(xí)的原因吧。

不過開發(fā)路漫漫,其實大家都知道,我們只不過是爬到巨人肩上的搬磚工人而已?;仡^看看自己腳下的路,每一塊磚都足以讓自己自慚形穢,自欺欺人什么的,也只不過是浮躁上頭,丟人現(xiàn)眼罷了。

所以筆者在寫本文的中途,已經(jīng)購買了《設(shè)計模式:可復(fù)用面向?qū)ο筌浖幕A(chǔ)》一書,希望能系統(tǒng)的學(xué)習(xí)一些代碼的構(gòu)建技巧吧(以后也好接著吹牛= =)。

后記的后記

筆者最近正在謀求一份新的工作,意向依舊是 iOS 開發(fā),坐標(biāo)仍舊是深圳。如果您有緣看到這篇文章,且希望我成為您并肩作戰(zhàn)的戰(zhàn)友,或是有個不錯的去處可以推薦的話,還希望您能與我聯(lián)系。在此先行謝過了~

責(zé)任編輯:chenqingxiang 來源: bifidy
相關(guān)推薦

2021-08-31 10:41:21

參數(shù)調(diào)優(yōu)代碼

2024-09-26 10:41:31

2015-02-27 10:14:33

2022-10-27 20:31:19

iptablesnetfilter

2023-11-06 07:45:42

單據(jù)圖片處理

2010-04-01 22:16:21

2017-06-12 11:09:56

計數(shù)架構(gòu)數(shù)據(jù)庫

2019-04-18 14:06:35

MySQL分庫分表數(shù)據(jù)庫

2025-04-11 03:00:55

2011-06-28 10:41:50

DBA

2020-09-23 06:52:49

代碼方法模式

2017-03-22 15:38:28

代碼架構(gòu)Java

2019-12-18 08:58:39

代碼變量名函數(shù)

2017-12-07 12:47:48

Serverless架構(gòu)基因

2018-05-25 14:41:56

Serverless無服務(wù)器構(gòu)造

2014-11-12 13:22:34

2019-07-15 10:22:40

HTTP分析CPU

2019-01-21 11:17:13

CPU優(yōu)化定位

2021-12-27 10:08:16

Python編程語言

2020-10-24 13:50:59

Python編程語言
點贊
收藏

51CTO技術(shù)棧公眾號