基于面向協(xié)議MVP模式下的軟件設(shè)計-(iOS篇)
- 傳統(tǒng)模式下的開發(fā)
- MVC
- MVVM
- 基于面向協(xié)議MVP的介紹
- MVP實戰(zhàn)開發(fā)
說在前面:
相信就算你是個iOS新手也應(yīng)該聽說過MVC的叔收,MVC是構(gòu)建iOS App的標準模板戏仓。隨著時間的推移,在iOS平臺上MVC也逐漸開始面臨著越來越多的問題,最近又開始流行MVVM,MVVM使由MVC衍生而來,MVVM作為一種新的開發(fā)模式和響應(yīng)式編程相結(jié)合用來解決一部分業(yè)務(wù)場景等绪商,今天坛掠,本我我要介紹給大家的是一個新的方式來架構(gòu)你的App: Model-View-Protocol,暫時可以理解為是基于協(xié)議的一種設(shè)計規(guī)范,拿出你的流行語bingo card,因為我們即將進行一次范式轉(zhuǎn)變盗忱。
一酱床、軟件設(shè)計鼻祖MVC
1.1、MVC
第一次聽到MVC這個名詞是在C#中,相信對于MVC大家都已經(jīng)很熟悉了,作為一種軟件設(shè)計模式,MVC這個概念已經(jīng)誕生好多年了趟佃。
如果你已經(jīng)開發(fā)一段時間的iOS應(yīng)用,你一定聽說過Model-View-Controller,在iOS開發(fā)中Apple從一開始就給我們引入這一理念,相信這個名詞大家都不陌生扇谣。
模型-視圖-控制器(Model-View-Controller,MVC)是Xerox PARC在20世紀80年代為編程語言Smalltalk-80發(fā)明的一種軟件設(shè)計模式,至今已廣泛應(yīng)用于用戶交互應(yīng)用程序中。在iOS開發(fā)中MVC的機制被使用的淋漓盡致,充分理解iOS的MVC模式,有助于我們程序的組織合理性闲昭。
Model-View-Controller
模型對象
模型對象封裝了應(yīng)用程序的數(shù)據(jù),并定義操控和處理該數(shù)據(jù)的邏輯和運算罐寨。例如,模型對象可能是表示商品數(shù)據(jù)list。用戶在視圖層中所進行的創(chuàng)建或修改數(shù)據(jù)的操作,通過控制器對象傳達出去,最終會創(chuàng)建或更新模型對象序矩。模型對象更改時(例如通過網(wǎng)絡(luò)連接接收到新數(shù)據(jù)),它通知控制器對象,控制器對象更新相應(yīng)的視圖對象鸯绿。
視圖對象
視圖對象是應(yīng)用程序中用戶可以看見的對象。視圖對象知道如何將自己繪制出來,并可能對用戶的操作作出響應(yīng)簸淀。視圖對象的主要目的,就是顯示來自應(yīng)用程序模型對象的數(shù)據(jù),并使該數(shù)據(jù)可被編輯瓶蝴。盡管如此,在 MVC 應(yīng)用程序中,視圖對象通常與模型對象分離。
在iOS應(yīng)用程序開發(fā)中,所有的控件租幕、窗口等都繼承自 UIView,對應(yīng)MVC中的V舷手。UIView及其子類主要負責UI的實現(xiàn),而UIView所產(chǎn)生的事件都可以采用委托的方式,交給UIViewController實現(xiàn)。
控制器對象
在應(yīng)用程序的一個或多個視圖對象和一個或多個模型對象之間,控制器對象充當媒介令蛉【鬯控制器對象因此是同步管道程序,通過它,視圖對象了解模型對象的更改,反之亦然,控制器主要負責數(shù)據(jù)的傳遞解耦等工作狡恬。控制器對象還可以為應(yīng)用程序執(zhí)行設(shè)置和協(xié)調(diào)任務(wù),并管理其他對象的生命周期蝎宇。
控制器對象解釋在視圖對象中進行的用戶操作,并將新的或更改過的數(shù)據(jù)傳達給模型對象弟劲。模型對象更改時,一個控制器對象會將新的模型數(shù)據(jù)傳達給視圖對象,以便視圖對象可以顯示它。
M和V永遠不能相互通信,只能通過控制器傳遞姥芥⊥闷颍控制器可以直接與Model對話(讀寫調(diào)用Model),Model通過通知或者KVO機制與控制器間接通信×固疲控制器可以直接與View對話,通過outlet,直接操作View,outlet直接對應(yīng)到View中的控件,View通過action向控制器報告事件的發(fā)生(如用戶的點擊事件)庸追。控制器是View的直接數(shù)據(jù)源(數(shù)據(jù)很可能是控制器從Model中取得并經(jīng)過加工了)台囱〉荩控制器是View的代理(delegate),以同步View與Controller。
MVC是一個用來組織代碼的權(quán)威范式,也是構(gòu)建iOS App的標準模式簿训。Apple甚至是這么說的咱娶。在MVC下,所有的對象被歸類為一個model,一個view,或一個controller。Model持有數(shù)據(jù),View顯示與用戶交互的界面,而View Controller調(diào)解Model和View之間的交互强品。然而,隨著模塊的迭代我們越來越發(fā)現(xiàn)MVC自身存在著很多不足膘侮。因此,MVVM從其他應(yīng)用而出,在 iOS中從此我們完全將業(yè)務(wù)邏輯加以區(qū)分并使用這套思想。
MVC在現(xiàn)實應(yīng)用中的不足
在上圖中,view將用戶交互通知給控制器的榛。view的控制器通過更新Model來反應(yīng)狀態(tài)的改變琼了。Model(通常使用Key-Value-Observation)通知控制器來更新他們負責的view。大多數(shù)iOS應(yīng)用程序的代碼使用這種方式來組織夫晌。
愈發(fā)笨重的Controller
在傳統(tǒng)的app中模型數(shù)據(jù)一般都很簡單,不涉及到復(fù)雜的業(yè)務(wù)數(shù)據(jù)邏輯處理,客戶端開發(fā)受限于它自身運行的的平臺終端,這一點注定使移動端不像PC前端那樣能夠處理大量的復(fù)雜的業(yè)務(wù)場景雕薪。然而隨著移動平臺的各種深入,我們不的不考慮這個問題。傳統(tǒng)的Model數(shù)據(jù)大多來源于網(wǎng)絡(luò)數(shù)據(jù),拿到網(wǎng)絡(luò)數(shù)據(jù)后客戶端要做的事情就是將數(shù)據(jù)直接按照順序畫在界面上慷丽。隨著業(yè)務(wù)的越來越來的深入,我們依賴的service服務(wù)可能在大多時間無法第一時間滿足客戶端需要的數(shù)據(jù)需求,移動端愈發(fā)的要自行處理一部分邏輯計算操作蹦哼。這個時間一慣的做法是在控制器中處理,最終導(dǎo)致了控制器成了垃圾箱,越來越不可維護。
控制器Controller是app的“膠水代碼”:協(xié)調(diào)模型和視圖之間的所有交互要糊「傺控制器負責管理他們所擁有的視圖的視圖層次結(jié)構(gòu),還要響應(yīng)視圖的loading、appearing锄俄、disappearing等等,同時往往也會充滿我們不愿暴露的Model的模型邏輯以及不愿暴露給視圖的業(yè)務(wù)邏輯局劲。這引出了第一個關(guān)于MVC的問題...
視圖view通常是UIKit控件(component,這里根據(jù)習(xí)慣譯為控件)或者編碼定義的UIKit控件的集合。進入.xib或者Storyboard會發(fā)現(xiàn)一個app奶赠、Button鱼填、Label都是由這些可視化的和可交互的控件組成。你懂的毅戈。View不應(yīng)該直接引用Model,并且僅僅通過IBAction事件引用controller苹丸。業(yè)務(wù)邏輯很明顯不歸入view,視圖本身沒有任何業(yè)務(wù)愤惰。
厚重的View Controller由于大量的代碼被放進viewcontroller,導(dǎo)致他們變的相當臃腫。在iOS中有的view controller里綿延成千上萬行代碼的事并不是前所未見的赘理。這些超重app的突出情況包括:厚重的View Controller很難維護(由于其龐大的規(guī)模)宦言;包含幾十個屬性,使他們的狀態(tài)難以管理;遵循許多協(xié)議(protocol),導(dǎo)致協(xié)議的響應(yīng)代碼和controller的邏輯代碼混淆在一起商模。
厚重的view controller很難測試,不管是手動測試或是使用單元測試,因為有太多可能的狀態(tài)奠旺。將代碼分解成更小的多個模塊通常是件好事。
太過于輕量級的Model
太過于輕量級的Model,早期的Model層,其實就是如果數(shù)據(jù)有幾個屬性,就定義幾個屬性,ARC普及以后我們在Model層的實現(xiàn)文件中基本上看不到代碼( 無需再手動管理釋放變量,Model既沒有復(fù)雜的業(yè)務(wù)處理,也沒有對象的構(gòu)造,基本上.m文件中的代碼普遍是空的)施流;同時與控制器的代碼越來厚重形成強烈的反差,這一度讓人不禁對現(xiàn)有的開發(fā)設(shè)計構(gòu)思有所懷疑响疚。
遺失的網(wǎng)絡(luò)邏輯
蘋果使用的MVC的定義是這么說的:所有的對象都可以被歸類為一個Model,一個view,或是一個控制器。就這些瞪醋。那么把網(wǎng)絡(luò)代碼放哪里忿晕?和一個API通信的代碼應(yīng)該放在哪兒?
你可能試著把它放在Model對象里,但是也會很棘手,因為網(wǎng)絡(luò)調(diào)用應(yīng)該使用異步,這樣如果一個網(wǎng)絡(luò)請求比持有它的Model生命周期更長,事情將變的復(fù)雜银受。顯然也不應(yīng)該把網(wǎng)絡(luò)代碼放在view里,因此只剩下控制器了杏糙。這同樣是個壞主意,因為這加劇了厚重控制器的問題。
那么應(yīng)該放在那里呢蚓土?顯然MVC的3大組件根本沒有適合放這些代碼的地方。
較差的可測試性
MVC的另一個大問題是,它不鼓勵開發(fā)人員編寫單元測試赖淤。由于控制器混合了視圖處理邏輯和業(yè)務(wù)邏輯,分離這些成分的單元測試成了一個艱巨的任務(wù)蜀漆。大多數(shù)人選擇忽略這個任務(wù),那就是不做任何測試。
上文提到了控制器可以管理視圖的層次結(jié)構(gòu)咱旱;控制器有一個“view”屬性,并且可以通過IBOutlet訪問視圖的任何子視圖确丢。當有很多outlet時這樣做不易于擴展,在某種意義上,最好不要使用子視圖控制器(child view controller)來幫助管理子視圖。
在這里有多個模糊的標準,似乎沒有人能完全達成一致吐限。貌似無論如何,view和對應(yīng)的controller都緊緊的耦合在一起,總之,還是會把它們當成一個組件來對待鲜侥。Apple提供的這個組件一度以來在某種程度誤導(dǎo)了大多初學(xué)者,初學(xué)者將所有的視圖全部拖到xib中,連接大量的IBoutLet輸出口屬性,都是一些列問題。
二诸典、大劍之初MVVM
在經(jīng)歷了一大堆吐槽之后,誕生了MVVM(一個高大尚牛逼哄哄的名詞,從此又多了一種人,你懂MVVM 描函?如果你的回答是否,瞬間被鄙視一把)。
新思維
其實MVVM據(jù)說最早在微軟的.NET平臺中出現(xiàn)過(具體什么背景,什么原因就不一一介紹了,還是要感謝偉大的.NET平臺工程師,造劍不如造經(jīng),世間萬道皆不離其宗),無論是是MVVM還是MVC我們無需堅持反對或者迷戀于它狐粱。在MVVM中他的設(shè)計思路和MVC很像舀寓。它正式規(guī)范了視圖和控制器緊耦合的性質(zhì),并引入新的組件。
Model-View-ViewModel
在理想的世界里,MVC也許工作的很好肌蜻。然而,我們生活在真實的世界互墓。既然我們已經(jīng)詳細說明了MVC在典型場景中的問題,那讓我們看一看一個可供替換的選擇:Model-View-ViewModel。
在MVVM里,view和view controller正式聯(lián)系在一起,我們把它們視為一個組件蒋搜。視圖view仍然不能直接引用模型Model,當然controller也不能篡撵。相反,他們引用視圖模型view Model判莉。
view Model是一個放置用戶輸入驗證邏輯,視圖顯示邏輯,發(fā)起網(wǎng)絡(luò)請求和其他各種各樣的代碼的極好的地方。有一件事情不應(yīng)歸入view Model,那就是任何視圖本身的引用育谬。view Model的概念同時適用于于iOS和OS X券盅。(換句話說,不要在view Model中使用 #import UIKit.h)
由于展示邏輯(presentation logic)放在了view Model中(比如Model的值映射到一個格式化的字符串),視圖控制器本身就會不再臃腫。當你開始使用MVVM的最好方式是,可以先將一小部分邏輯放入視圖模型,然后當你逐漸習(xí)慣于使用這個范式的時候再遷移更多的邏輯到視圖模型中斑司。
以我的經(jīng)驗,使用MVVM會輕微的增加代碼量,但總體上減少了代碼的復(fù)雜性渗饮。這是一個劃算的交易。
回過頭再來看MVVM的圖示,你會注意到我使用了模糊的動詞“notify”和“update”,而沒有詳細說明該怎么做宿刮。你可以使用KVO,就像MVC那樣,但這很快就會變得難以管理互站。事實上,使用ReactiveCocoa會是更好的方式來組織各個部分。
關(guān)于怎么結(jié)合ReactiveCocoa來使用MVVM的信息,可以閱讀開源app僵缺。你也可以閱讀我的關(guān)于ReactiveCocoa和MVVM的書胡桃。
關(guān)于MVVM的具體細節(jié)此處就不多詳細介紹,此處重在做對比分析,如果了解的可以去了解相關(guān)資料。
三磕潮、基于面向協(xié)議MVP的介紹
曾經(jīng)有無數(shù)個人總喜歡問我你們的iOS采用什么樣的架構(gòu),其實每次被問到這樣的問題,不是瞬間被蒙了,就是想自己問自己iOS也有架構(gòu)翠胰??
上文提到了MVC自脯、MVVM,真實的業(yè)務(wù)場景中,如果場景的邏輯異常復(fù)雜,在反復(fù)的迭代中仍會出現(xiàn)各式各樣的問題之景。真對MVVM我個人理解主要是將原來Controller中處理數(shù)據(jù)邏輯的代碼統(tǒng)一歸到一個新的class(viewModel)中去,更甚之網(wǎng)絡(luò)請求等工作全部從Controller移到viewModel。剛一開始總覺的怪怪的「喑保現(xiàn)階段客戶端開發(fā)越來越進入一個2.0的階段,早期的app功能都相對比較簡單,無論是從界面還是從業(yè)務(wù)邏輯上給人的感覺都是簡潔實用,這中間包括UI的設(shè)計锻狗、功能的設(shè)計、產(chǎn)品的設(shè)計定位等焕参。隨著行業(yè)的深入,用戶的過渡依賴移動端最終導(dǎo)致業(yè)各式各樣的業(yè)務(wù)更加依賴客戶端,這就導(dǎo)致客戶端的開發(fā)不得不向PC端靠齊,在版本的反復(fù)迭代中業(yè)務(wù)場景變的愈發(fā)不盡人意,仿佛又回到了軟件設(shè)計的早期轻纪。
在傳統(tǒng)軟件領(lǐng)域,從MVC的誕生主要是為了解決軟件界面的行為的分離,在復(fù)雜的業(yè)務(wù)場景內(nèi)會進一步區(qū)分業(yè)務(wù)邏輯場景的分離,這些手段的最終的目的都是盡最大限度的降低整個場景的藕合度,使其達到分離的目的,模塊與模塊最終得到獨立,將整個場景化整為零,最終使每個模塊在一個零上工作,這對于無論是軟件的開發(fā)還是后續(xù)的維護、以及使用普遍遵循這個原則,現(xiàn)有的模式大概產(chǎn)生了相關(guān)的類似架構(gòu)叠纷。
傳統(tǒng)web架構(gòu)里面是這樣解決的 :
- web段以及其他業(yè)務(wù)層負責從接口層獲取數(shù)據(jù)并執(zhí)行自己的邏輯
- service層為外部提供接口
- DTO從負責從DB鏈接并進行數(shù)據(jù)讀寫操作
- DB層(物理機負責數(shù)據(jù)存儲)
現(xiàn)有客戶度一度采用下面的模式:
客戶端通過service拿到j(luò)son 數(shù)據(jù),然后通過MVC的結(jié)構(gòu)展示到UI界面上,在iOS中一直流行MVC的開發(fā)模式,通過與傳統(tǒng)開發(fā)模式對比可以發(fā)現(xiàn),其實
service層-客戶端交互與服務(wù)端service服務(wù)滿足外部業(yè)務(wù)場景無非是兩個互逆的過程(一個輸出層,一個輸入層,都是為了更好的滿足的下一步的業(yè)務(wù)需求,一個是將原始數(shù)據(jù)邏輯話,一個是將獲得邏輯數(shù)據(jù)存檔并且展示到用戶面前)刻帚。service層根據(jù)具體的業(yè)務(wù)場景提供對應(yīng)的數(shù)據(jù)服務(wù),service根據(jù)不同的業(yè)務(wù)場景通過DTO層拿到對應(yīng)
的數(shù)據(jù)然后組織好數(shù)據(jù)提供給外界(service 層負責將原始物理數(shù)據(jù)轉(zhuǎn)換成對應(yīng)的邏輯數(shù)據(jù)提供給外界)。
相反,客戶端通過網(wǎng)絡(luò)層拿到對應(yīng)的網(wǎng)絡(luò)數(shù)據(jù)繪制到對應(yīng)的View上,但是實際的開發(fā)過程中,網(wǎng)絡(luò)數(shù)據(jù)與真實客戶端使用場景也是有一定的差距,MVVM層將對應(yīng)的
一部分邏輯處理移植到了ViewModel中,這并沒有從根本上解決問題,無非是將代碼做了一份對應(yīng)的copy轉(zhuǎn)移,并沒有從根本上達到邏輯分層的概念涩嚣。相反MVP模
式恰好解決了這一難題,MVP模式衍生于傳統(tǒng)service架構(gòu),針對不同的業(yè)務(wù)場景圖供對應(yīng)的匹配的抽象service服務(wù),客戶端拿到網(wǎng)絡(luò)數(shù)據(jù)后未達到指定的目的,
為滿足相同抽象邏輯的業(yè)務(wù)場景,在客戶端網(wǎng)絡(luò)層與Model層之間加一協(xié)議層,Model層實現(xiàn)整個協(xié)議層,之后在基于MVC的結(jié)構(gòu)下將一概相同層次的
業(yè)務(wù)場景繪制解釋到對應(yīng)的View上崇众。
- M : 邏輯Model層
- V : 視圖層
- P : protocol協(xié)議層
Model層類似于MVVM的ViewModel,主要負責存儲抽象邏輯數(shù)據(jù),另外Model層主還有部分工作實現(xiàn)對應(yīng)的協(xié)議層協(xié)議,提供協(xié)議對應(yīng)的各種屬性以及服務(wù)。Model經(jīng)過協(xié)議層抽象約束,最后Model被抽象成具有統(tǒng)一抽象邏輯的業(yè)務(wù)場景,最終Model層在講數(shù)據(jù)交付整個MVC結(jié)構(gòu)繪制展示的時間,我們可以按照同一套抽象的邏輯標準去執(zhí)行缓艳。
在傳統(tǒng)的web層面,為了滿足各式各樣的業(yè)務(wù)邏輯場景服務(wù),最紅我們實現(xiàn)軟件羅杰的層次的分離,誕生了service服務(wù)這個概念(service就類似一個標準尺寸的水龍頭出口,只要對應(yīng)的水龍頭都按照這樣的規(guī)則來生產(chǎn),service就能夠滿足格式各樣的業(yè)務(wù)場景,極大的解決的傳統(tǒng)軟件服務(wù)業(yè)務(wù)場景層次的一系列難題)校摩;相同的原理在客戶端同樣可以使用,為了滿足客戶端MVC結(jié)構(gòu)層里面的穩(wěn)定,避免各式各樣的業(yè)務(wù)場景迭代插入不同的邏輯,避免最終軟件危機的產(chǎn)生,我們采用追加協(xié)議層的模式來滿足這一目的。
遍觀整個軟件開發(fā),從早期的軟件開發(fā),到后來軟件生產(chǎn)管理的危機,軟件開發(fā)模式一步步的確立,軟件行業(yè)的每個階段都是一個里程碑阶淘。這世間沒有相對完美獨到的設(shè)計法則,但是亙古不變永遠只有一個那就是軟件的開發(fā)更佳面相生產(chǎn)化衙吩、規(guī)范化、更加的利于可維護化溪窒。一直以來我本人并不特別的注重軟件的設(shè)計一定坤塞、必須按照某種規(guī)則來做,畢竟不同的人冯勉、不同的業(yè)務(wù)場景、不同的工程師總有不同的實際境況,站在一個開發(fā)工程師的角度來說我并不固執(zhí)于都按照固定的規(guī)則來(比如說你必須按照某個模式來做,必須用MVVM來做摹芙;必須用ReactCocoa信號型機制來做...)灼狰。相反我個人認為太過于固執(zhí)只不過某些人的一廂情愿的罷了。相反我覺得因地制宜浮禾、應(yīng)運而生豈不更加快哉,設(shè)計不拘于模式,更多時間更是不局限于思考交胚。無論是MVVM、MVP哪一個不是脫胎于MVC,這個世間萬變不離其宗,萬千功法始終都離不開一部最終的母經(jīng)盈电。
四蝴簇、MVP實戰(zhàn)開發(fā)
說了這么多,下面上實戰(zhàn)例子。
大概描述一下業(yè)務(wù)場景,作為電商app,我們希望在原生的基礎(chǔ)上開發(fā)一套定制的可控匆帚、可配熬词、可維護的通用型原生模版(至于說的這么靈活 有多么的好,為啥不用H5、ReactNative,這個問題不要來問我,產(chǎn)品狗們讓做原生,程序員只能執(zhí)行)吸重。大概是這樣一個場景,可以配置的樓層樣式多達十幾種(至少目前已經(jīng)有十幾種,以后可能會更多)互拾;每種可配置樓層樣式是多元的,外觀長相不一,數(shù)據(jù)格式也不盡相同但有部分類同;要求后臺CMS配置界面配置法則有共同相似之處嚎幸;要求每種樣式樓層處理事件記憶跳轉(zhuǎn)不盡相同颜矿;最可恨的頁面已經(jīng)很長了以后會源源不斷加入新的模版。
考慮到長遠,這樣的復(fù)雜樓層,如果仍舊按照傳統(tǒng)的模式來做,問題會很多,數(shù)據(jù)無法統(tǒng)一嫉晶、無法統(tǒng)一繪制或衡、無法統(tǒng)一處理。具體場景相信大家應(yīng)該理解了车遂。
上設(shè)計思路
潛在問題
- server段需要針對不同的樓層場景下發(fā)不同的數(shù)據(jù) 數(shù)據(jù)結(jié)構(gòu)不盡相同
- 模版樓層樣式不盡相同 可能對應(yīng)多種View
- 多種View與多種數(shù)據(jù)結(jié)構(gòu)的解釋解耦問題
- 多種業(yè)務(wù)場景用戶操作邏輯處理問題
- 樓層過于復(fù)雜 Controller代碼大爆炸
邏輯建模分析
暫時可以將每種模版樓層的整體數(shù)據(jù)作為一個容器Modle,主要負責該樓層的整體數(shù)據(jù)調(diào)度
將每種樓層公有的屬性以及內(nèi)容抽象出來放入一個容器父類Container,然后將不同模版特有的屬性放在子模版派生Model中,作為派生屬性
對準一個容器類,我可以將每種容器Model的使用法則抽象總結(jié)歸納(1、樓層是否有Header,是否要吸頂斯辰;2舶担、該樓層具體要由什么樣的View模版去繪制; 3、樓層內(nèi)容是繪制在單個section單個cell中還是繪制在多行上; 4彬呻、每個樓層的元素點擊跳轉(zhuǎn)處理等). 我們將容器這塊作為一個數(shù)據(jù)源概念最終抽象出一套可供外界獲取數(shù)據(jù)的Interface(Protocol)
當我們拿到樓層數(shù)據(jù)后在父容器基礎(chǔ)上做子樓層的派生,我們要求派生容器去實現(xiàn)上述父容器的Protocol,通過協(xié)議我們可以知道具體的繪制的目標,以及要繪制的元素個數(shù)等,最終達到一個目的,
將每個樓層的數(shù)據(jù)裝配在我們定義好的一個適配器容器內(nèi),然后通過協(xié)議給外界提供一套統(tǒng)一的操作入口,之后我們才用統(tǒng)一的操作方式操作容器,最終實現(xiàn)一個容器對應(yīng)一個樓層衣陶。Render 協(xié)議,在這個我們對準每個要具體繪制到UI上的Model,我們統(tǒng)一讓其實現(xiàn)Render協(xié)議,通過適配器容器我們我們拿到具體要繪制的目標,
目標繪制題都實現(xiàn)了Render協(xié)議,在Render協(xié)議我們可以拿到具體當前Model將由哪個具體的Cell去呈遞。在每個繪制目標題內(nèi)由Model決定
當前內(nèi)容由什么樣式的cell模版去繪制闸氮。我們把所有的樓層數(shù)據(jù)處理邏輯壓在適配器容器內(nèi),再將Model的繪制目標都交由Model自己決定剪况。實現(xiàn)上述目標后,在ViewController層面,我們看到的只有一個實現(xiàn)了適配器協(xié)議的Model數(shù)組,在 table的繪制過程我們通過操作一個
id<適配器Protocol> 類型的Model對象, 拿到這個具體的索引對應(yīng)的對象后,通過內(nèi)部已經(jīng)實現(xiàn)的協(xié)議我們很快的拿到下一個要繪制的目標Model
然后再拿到具體的Cell模版的Identifier,然后從tableview中取到當前Identifier對應(yīng)的cell模版,傳入數(shù)據(jù)最后返回一個cell。
這個地方我們先定義了一個適配容器協(xié)議,以及一個父容器類,我們將樓層公有屬性放在父類中,將派生容器子類(具體的樓層Model)實現(xiàn)一個抽象協(xié)議蒲跨。
MVP模式的原則是译断,在service層提供一大堆不盡相同的業(yè)務(wù)場景之后,我們將這一系列數(shù)據(jù)全部抽象歸納或悲,通過定制一套標準的protocol協(xié)議孙咪,讓不同業(yè)務(wù)場景的都去實現(xiàn)這一協(xié)議堪唐,最終將數(shù)據(jù)全部裝配、拼裝成一套具有相同調(diào)度規(guī)則的統(tǒng)一編制話數(shù)據(jù)Model翎蹈,之后我們采用id<protocol>的標準去操作數(shù)據(jù)淮菠。
MVP除了將數(shù)據(jù)邏輯完全鎮(zhèn)封的各自的Model,同時將那些Model需要繪制,哪些Model需要校驗, 哪些Model需要接受處理點擊Action這些邏輯全部由Model自己來決定荤堪,Controller只作為一個粘合性的模版合陵,Controller只處理一批具有共性的范型類數(shù)據(jù),至于具體的操作操作毫不關(guān)心澄阳。
MVP面相的更多的是在MVC上層與service之間追加了一層協(xié)議層拥知,我們認為通過協(xié)議層處理過的數(shù)據(jù)是暫時可以客戶端場景使用的數(shù)據(jù),在數(shù)據(jù)到達MVC我們針對數(shù)據(jù)進行再加工寇荧、再構(gòu)造處理举庶,這一切全部在容器內(nèi)操作。這一點完全與別的設(shè)計模式相反揩抡。
MVP并不是讓用戶在Model打上網(wǎng)絡(luò)請求操作户侥、在Model層執(zhí)行[self.navigationController pushViewController:***]等這些操作,其實相對大多人來說對于部分對象生命周期長短問題還是很在乎峦嗤,所以在處理TemplateActionProtocol協(xié)議的時間蕊唐,MVP只是對準Action做了一層抽象封裝。通過實現(xiàn)TemplateActionProtocol的Model會產(chǎn)生一個Action對象烁设,最終通過block的調(diào)用鏈傳回控制器中,我們在控制器中統(tǒng)一做handler處理替梨。
MVP我們最初設(shè)計目的就是為了強調(diào)一個裝配概念,如果發(fā)生了業(yè)務(wù)場景的追加,控制器我不會改動其中的代碼装黑,只需要將新數(shù)據(jù)追加成相同批次的ViewModel副瀑,然后配置進容器,之后控制器不做任何修改就可以滿足需求了恋谭。通過具體的剝離糠睡、抽取我們成功了的最大限度的剝離了控制器,滿足了輕量級Controller這一概念疚颊。
MVP與傳統(tǒng)軟件相比,在設(shè)計這一點的時間我們完全借鑒了傳統(tǒng)軟件的思維模式狈孔,Java平臺的service設(shè)計模式、三層架構(gòu)這些設(shè)計規(guī)范都相對做了一些對比分析材义,最終得出了MVP這一理念均抽。
分析場景完畢,下面來分析一個模版的例子來說命一切吧!其掂!
Protocol 設(shè)計
Template 協(xié)議
- -TemplateRenderProtocol //任何一個具體繪制到cell上的Model都需要實現(xiàn)該協(xié)議
- -TemplateSorbRenderProtocol //樓層的header如果要吸頂需要使用該協(xié)議替代基本的Render協(xié)議
- -TemplateActionProtocol //具有Action 操作的Model需要實現(xiàn),返回一個通用的Action對象
- -TemplateCellProtocol //整個體系中所有的Cell統(tǒng)一實現(xiàn)該協(xié)議
- TemplateValidationProtocol //- 處理一部分認證校驗數(shù)據(jù)
ViewController
- 樓層顯示統(tǒng)一交與Model層定制
- VC中生成ActionModel,跳轉(zhuǎn)邏輯全部應(yīng)用于Action協(xié)議層,ViewController實現(xiàn)ActionManager 代理,作為回調(diào)處理
- 特定屬性處理邏輯放在分類內(nèi)
- 網(wǎng)絡(luò)層調(diào)用扔保持在ViewController,這一點與傳統(tǒng)保持相似,有利于結(jié)構(gòu)分明(優(yōu)于市面上的所謂MVVM)
//TemplateRenderProtocol.h
@protocol TemplateRenderProtocol <NSObject,TemplateActionProtocol>
@required
- (NSString *)floorIdentifier;
@end
//TemplateSorbRenderProtocol.h
@protocol TemplateSorbRenderProtocol <NSObject>
- (NSString *)headerFloorIdentifier;
- (id <TemplateSorbRenderProtocol>)headerFloorModelAtIndex:(NSInteger)index;
@end
//TemplateActionProtocol.h
*/
@protocol TemplateActionProtocol <NSObject>
@optional
- (TemplateAction *)jumpFloorModelAtIndexPath:(NSIndexPath *)indexPath;
@end
//TemplateCellProtocol.h
@protocol TemplateBaseProtocol;
typedef void (^TapBlock) (NSIndexPath* index);
@protocol TemplateCellProtocol <NSObject>
@optional
+ (CGSize)calculateSizeWithData:(id<TemplateRenderProtocol>)data constrainedToSize:(CGSize)size;
- (void)processData:(id <TemplateRenderProtocol>)data;
- (void)tapOnePlace:(TapBlock) block;
@end
Model設(shè)計
// TemplateContainerModel.h
/**
* 容器概念
*/
@protocol TemplateContainerProtocol <NSObject>
@required
- (NSInteger)numberOfChildModelsInContainer;
- (id <TemplateRenderProtocol>)childFloorModelAtIndex:(NSInteger)index;
@end
@class TemplateChannelModel;
@interface TemplateContainerModel : NSObject<TemplateContainerProtocol,TemplateActionProtocol,TemplateRenderProtocol>
//netList
@property (nonatomic,strong) NSNumber *identityId;
@property (nonatomic,strong) NSString *pattern;
@property (nonatomic,strong) TemplateFHeaderModel *fheader;
@property (nonatomic,strong) NSArray *itemList;
@property (nonatomic,strong) TemplateJumpModel *jump;
@property (nonatomic,strong) TemplateMarginModel *margin;
//other add
@property (nonatomic,assign) TemplateChannelModel *channelModel;
@end
下面的就先引用一個具體的業(yè)務(wù)場景吧,頂部banner樓層,每個大的樓層都是一個容器Model,是繼承于父容器,并且會適當重寫父類協(xié)議以及方法
//TemplateFloorFocusModel.h
//此處,banner是多個對象繪制成輪播的樣式,整體是繪制在同一個cell上的,所以TemplateFloorFocusModel首先是一個容器類,是具有數(shù)據(jù)源的
功能,但是他又是一個繪制目標Model,TemplateFloorFocusModel實現(xiàn)了Render協(xié)議,就決定這個接下來會將TemplateFloorFocusModel繪制到UI界面上(如果此處的容器存儲的是一個section下的list形式,容器類就無需實現(xiàn)render協(xié)議,只需要將list 中的Model實現(xiàn)render協(xié)議即可)
@interface TemplateFloorFocusModel : TemplateContainerModel<TemplateRenderProtocol>
@property (nonatomic,assign) NSNumber *width;
@property (nonatomic,assign) NSNumber *height;
@end
//TemplateFloorFocusModel.m
@implementation TemplateFloorFocusModel
+ (NSDictionary *)mj_replacedKeyFromPropertyName
{
return @{
@"itemList" : @"picList"
};
}
+ (NSDictionary *)mj_objectClassInArray
{
return @{
@"itemList" : @"TemplatePicModel"
};
}
//pragma mark - TemplateContainerProtocol
- (NSInteger)numberOfChildModelsInContainer
{
NSUInteger rows = 0;
if (self.margin) rows++;
if (self.itemList) rows++;
return rows;
}
//(如果此處的容器存儲的是一個section下的list形式,此處返回一個實現(xiàn)render協(xié)議的Model即可)
- (id <TemplateRenderProtocol>)childFloorModelAtIndex:(NSInteger)index
{
if ((self.margin)&&(index+1) == [self numberOfChildModelsInContainer])
return self.margin; //最后一行
return self;
}
//pragma mark - TemplateActionProtocol
- (TemplateAction *)jumpFloorModelAtIndexPath:(NSIndexPath *)indexPath
{
NSUInteger position = [indexPath indexAtPosition:0];
if (position < self.itemList.count) {
TemplatePicModel *picModel = self.itemList[position];
TemplateJumpAction *action = [[TemplateJumpAction alloc] init];
action.jumpToType = TemplateJumpToActivityM;
action.jumpToUrl = picModel.jump.url;
action.eventId = @"GeneralChannel_BannerPic";
return action;
}
return nil;
}
//pragma mark - TemplateRenderProtocol
- (NSString *)floorIdentifier
{
return @"TemplateFocusCell";
}
View 設(shè)計
View設(shè)計此處我們才用方式依舊是將Cell作為模版,將對應(yīng)的視圖邏輯統(tǒng)一放在一個UIViewSubView中, 之后在Cell中將View直接add到cell.ContentView上油挥。
針對焦點圖cell TemplateFocusCell我們有一個TemplateFocusView來對應(yīng),下面看下代碼設(shè)計
TemplateFocusCell
// TemplateFocusCell
@interface TemplateFocusCell : UITableViewCell<TemplateCellProtocol>
@end
@interface TemplateFocusCell (){
TemplateFocusView *_focusView;
}
@property (nonatomic,strong) id <TemplateRenderProtocol> data;
@end
@implementation TemplateFocusCell
- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier
{
self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
if (self) {
_focusView = [[TemplateFocusView alloc] init];
_focusView.translatesAutoresizingMaskIntoConstraints = NO;
[self.contentView addSubview:_focusView];
}
return self;
}
- (void)processData:(id <TemplateRenderProtocol>)data
{
if([data isKindOfClass:[TemplateFloorFocusModel class]])
{
self.data = data;
[_focusView processData:(id <TemplateRenderProtocol>)data];
}
}
+ (CGSize)calculateSizeWithData:(id<NSObject>)data constrainedToSize:(CGSize)size
{
// id<TemplateRenderProtocol> model = data;
CGSize curSize = CGSizeMake(ScreenWidth, 110);
return curSize;
}
- (void)tapOnePlace:(TapBlock) block
{
[_focusView setTapBlock:block];
}
TemplateFocusView
@interface TemplateFocusView : UIView<TemplateCellProtocol>
@end
@interface TemplateFocusView ()<iCarouselDataSource,iCarouselDelegate>
{
UIPageControl *_pageControl;
iCarousel *_scrollView;
}
@property (nonatomic,strong) TemplateFloorFocusModel *focusModel;
@end
@implementation TemplateFocusView
- (instancetype)init
{
self = [super init];
if (self)
{
_scrollView = [[iCarousel alloc] init];
_scrollView.delegate = self;
_scrollView.dataSource = self;
_scrollView.type = iCarouselTypeLinear;
_scrollView.pagingEnabled = YES;
_scrollView.bounceDistance = 0.5;
_scrollView.decelerationRate = 0.5;
_scrollView.clipsToBounds = YES;
_scrollView.translatesAutoresizingMaskIntoConstraints = NO;
[self addSubview:_scrollView];
_pageControl = [[UIPageControl alloc] init];
_pageControl.translatesAutoresizingMaskIntoConstraints = NO;
[self addSubview:_pageControl];
[_scrollView mas_makeConstraints:^(MASConstraintMaker *make){
make.edges.equalTo(self).insets(UIEdgeInsetsZero);
}];
[_pageControl mas_makeConstraints:^(MASConstraintMaker *make){
make.bottom.mas_equalTo(@(5));
make.centerX.equalTo(self);
}];
}
return self;
}
+ (CGSize)calculateSizeWithData:(id<TemplateRenderProtocol>)data constrainedToSize:(CGSize)size
{
return size;
}
- (void)processData:(id <TemplateRenderProtocol>)data
{
self.focusModel = (TemplateFloorFocusModel *)data;
_pageControl.numberOfPages = self.focusModel.itemList.count;
[_scrollView reloadData];
[self layoutIfNeeded];
}
//pragma mark -
- (NSInteger)numberOfItemsInCarousel:(iCarousel *)carousel
{
return _focusModel.itemList.count;
}
- (UIView *)carousel:(iCarousel *)carousel viewForItemAtIndex:(NSInteger)index reusingView:(UIView *)view
{
UIImageView *imageView = nil;
if (!view) {
imageView = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, ScreenWidth, ScreenWidth/2)];
imageView.contentMode = UIViewContentModeScaleAspectFit;
}else{
imageView = (UIImageView *)view;
}
TemplatePicModel *model = self.focusModel.itemList[index];
[imageView setImageWithURL:[NSURL URLWithString:model.img]];
return imageView;
}
- (CGFloat)carousel:(iCarousel *)carousel valueForOption:(iCarouselOption)option withDefault:(CGFloat)value
{
if (option == iCarouselOptionWrap)
{
return YES;
}
return value;
}
- (void)carouselDidEndScrollingAnimation:(iCarousel *)carousel
{
NSInteger index = _scrollView.scrollOffset;
[_pageControl setCurrentPage:index];
}
- (void)carousel:(iCarousel *)carousel didSelectItemAtIndex:(NSInteger)index
{
NSIndexPath *indexPath = [NSIndexPath indexPathWithIndex:index];
if (_tapBlock) {
_tapBlock(indexPath);
}
}
從View層可以看到,我們?nèi)耘f遵循以往的模式,將cell高度的計算,最終放在View中來完成(此處我們并沒有Model化,而是仍舊遵循大家的習(xí)慣,具體的高度根據(jù)具體的視圖場景來控制),看到此處的計算高度的方法,接下來的問題就不多說了....
Controller 設(shè)計
在做完以上的一些列的邏輯化抽象工作以后,從新回到控制器層面,此時應(yīng)該是大松了一口氣了,到目前為止,我們一大堆系列的工作都已經(jīng)做完了,只是還有一點失望的感覺是暫時還沒看到是否真的有卵用,這就好比十年鑄一劍,繼而十年在磨一劍,看不到成效始終覺得心中似有虧欠。
到目前為止,我們在控制器層面能做的僅有的是范型數(shù)據(jù)的操作,已經(jīng)安全沒有邏輯了,邏輯全部壓入了Model,下面就看下控制器層面的邏輯:
//處理action
- (TapBlock)tapBlockForModel:(id<TemplateRenderProtocol>)model
{
__weak typeof (self) weakself = self;
return ^(NSIndexPath * indexPath){
if ([model conformsToProtocol:@protocol(TemplateActionProtocol)]) {
TemplateAction *action = [(id<TemplateActionProtocol>)model jumpFloorModelAtIndexPath:indexPath];
[weakself.handler handlerAction:action];
}
};
}
//注冊cell
[self.tableView registerClass:[TemplateFocusCell class] forCellReuseIdentifier:@"TemplateFocusCell"];
//tableView 代理實現(xiàn)
//pragma mark - UITableViewDataSource,UITableViewDelegate
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
return [self.floorModel.floors count];
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
TemplateContainerModel<TemplateContainerProtocol> *list = self.floorModel.floors[section];
return [list numberOfChildModelsInContainer];
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
id <TemplateRenderProtocol> model = [self.floorModel rowModelAtIndexPath:indexPath];
UITableViewCell <TemplateCellProtocol> * cell = [tableView dequeueReusableCellWithIdentifier:[model floorIdentifier]];
[cell processData:model];
[cell tapOnePlace:[self tapBlockForModel:model]];
if(!cell){
return [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:nil];
}else{
return (UITableViewCell *)cell;
}
}
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath{
id <TemplateRenderProtocol> floor = [self.floorModel rowModelAtIndexPath:indexPath];
if ([floor respondsToSelector:@selector(floorIdentifier)]) {
NSString *cellIdentifier = [floor floorIdentifier];
Class<TemplateCellProtocol> viewClass = NSClassFromString(cellIdentifier);
CGSize size = [viewClass calculateSizeWithData:floor constrainedToSize:CGSizeMake(tableView.frame.size.width, 0.0)];
return size.height;
}
return 0;
}
- (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section
{
id <TemplateSorbRenderProtocol,TemplateRenderProtocol> floor = self.floorModel.floors[section];
if ([floor conformsToProtocol:@protocol(TemplateSorbRenderProtocol)]) {
NSString *headerIdentifier = [floor headerFloorIdentifier];
if (headerIdentifier) {
Class<TemplateCellProtocol> viewClass = NSClassFromString(headerIdentifier);
CGSize size = [viewClass calculateSizeWithData:floor constrainedToSize:CGSizeMake(tableView.frame.size.width, 0.0)];
return size.height;
}
}
return 0;
}
- (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section
{
id <TemplateSorbRenderProtocol,TemplateRenderProtocol> floor = self.floorModel.floors[section];
if ([floor conformsToProtocol:@protocol(TemplateSorbRenderProtocol)]) {
id<TemplateSorbRenderProtocol> headerModel = [floor headerFloorModelAtIndex:section];
if (headerModel) {
NSString *identifier = [headerModel headerFloorIdentifier];
UIView <TemplateCellProtocol> *headerView = (UIView <TemplateCellProtocol> *)[tableView dequeueReusableHeaderFooterViewWithIdentifier:identifier];
[headerView processData:floor];
return headerView;
}
}
return nil;
}
至此,控制器只剩下以上操作,相對來說已經(jīng)最大限度的梳理了邏輯,將所有的邏輯壓入Model,如果服務(wù)端新增了新型的業(yè)務(wù)場景的數(shù)據(jù),依舊可以通過協(xié)議層的適配,將數(shù)據(jù)最終的組裝上述模式,最后直接拿來使用,如果需要修改對應(yīng)的View,直接可以在Model內(nèi)修改具體的將要渲染的View的名字即可,這些工作都跟控制器層沒有任何關(guān)系。
在Action協(xié)議中,具有Action操作的Model會在使用過程中實現(xiàn)TemplateActionProtocol這一協(xié)議,在事件處理的時間會拋出這樣一個ActionModel,之后此處我們會直接對Action對象handler操作,此處并沒有控制器層UI界面的操作,這一點遵循了設(shè)計模式中的命令行模式(這一點原理脫胎于于strus框架中XWork框架,將控制器與UI工作無關(guān)的內(nèi)務(wù)以命令行的模式跑出來,放在別的一個代理中去完成,這樣能夠最大的限度的做到對控制器層面的瘦身工作)喘漏。
說到控制器瘦身工作,iOS常用的大概是就是Category了,將部分全局型屬性护蝶、邏輯放在對應(yīng)的分類里面,有助于邏輯的抽離、代碼的分割翩迈。
容器模式 適配器模式 命令行模式
用Model-View-ViewModel構(gòu)建iOS App