在我前面的兩篇文章里面分別對(duì)MVC框架中的M層的定義和構(gòu)建方法進(jìn)行了深入的介紹和探討。這篇文章則是想深入的介紹一下我們應(yīng)該如何去構(gòu)建控制層。控制層是聯(lián)系視圖層和模型層的紐帶。現(xiàn)在也有非常多的文章宣揚(yáng)所謂的去控制層或者弱化控制層的作用,覺(jué)得這部分是一個(gè)雞肋,他會(huì)使得應(yīng)用變得臃腫不堪。那么他是否有存在的必要呢?
一般的應(yīng)用場(chǎng)景里面,我們都需要將各種界面呈現(xiàn)給用戶,然后用戶通過(guò)某些操作來(lái)達(dá)到某個(gè)目標(biāo)。從上面的場(chǎng)景中可以提取出呈現(xiàn)、操作、目標(biāo)三個(gè)關(guān)鍵字。要呈現(xiàn)出什么以及要完成什么目標(biāo)我們必須要通過(guò)具體操作才能達(dá)成,也就是說(shuō)是通過(guò)操作來(lái)驅(qū)動(dòng)界面的不斷變化以及服務(wù)目標(biāo)的不斷達(dá)成,操作是聯(lián)系界面和目標(biāo)的紐帶。為了表征這種真實(shí)的場(chǎng)景,在軟件建模和設(shè)計(jì)實(shí)現(xiàn)中也應(yīng)如此。我想這也就是MVC框架這種應(yīng)用模型設(shè)計(jì)的初衷吧。在MVC框架中V負(fù)責(zé)呈現(xiàn)C負(fù)責(zé)操作而M則負(fù)責(zé)目標(biāo)。而且這種設(shè)計(jì)還有如下更多的考量:
- 視圖界面千變?nèi)f化,會(huì)根據(jù)用戶的體驗(yàn)不停的升級(jí)和優(yōu)化,甚至同一個(gè)功能的前后兩個(gè)版本都有完全不同的差異,或者某些視圖界面會(huì)分散到其他視圖界面中去,又或原來(lái)分散的視圖界面又聚合到某個(gè)新視圖界面中來(lái)。也就是說(shuō)視圖呈現(xiàn)部分是變化最大以及持久性最短的一個(gè)部分。
- 模型(服務(wù))則相對(duì)穩(wěn)定,他只是提供了某些具體的基礎(chǔ)業(yè)務(wù)服務(wù),而且這些服務(wù)更多的是服務(wù)水平的升級(jí)而非服務(wù)的完全改變。因此模型部分是變化最小且持久性最長(zhǎng)的一個(gè)部分。
- 一般情況下我們對(duì)視圖界面上的操作控制需要調(diào)用多個(gè)服務(wù)來(lái)完成,或者不同的界面上的呈現(xiàn)可能會(huì)由同一個(gè)服務(wù)來(lái)支撐。因此我們不能將界面呈現(xiàn)和服務(wù)目標(biāo)進(jìn)行一對(duì)一的強(qiáng)行綁定,我們需要將呈現(xiàn)和模型進(jìn)行解耦處理。
控制層的引入正是解決了上面的這些矛盾,他將視圖和模型的關(guān)聯(lián)減少到最低,同時(shí)也是將易變的和不變這種矛盾體進(jìn)行了化解。控制層就是一個(gè)中介者(參考設(shè)計(jì)模式中的中介者模式)我們應(yīng)該把具體的操作交給控制層來(lái)完成,并且由控制層來(lái)驅(qū)動(dòng)視圖的呈現(xiàn)和服務(wù)的提供。這看來(lái)好像是一種最優(yōu)的解決方案。
控制器--功能的劃分邊界
那么控制層除了具備處理操作以及實(shí)現(xiàn)視圖和模型之間聯(lián)系的紐帶之外,還應(yīng)該具有什么特征呢?
應(yīng)用程序從使用者的角度來(lái)看他其實(shí)就是能夠提供某種能力的功能的集合。每個(gè)功能都具有對(duì)應(yīng)的展示效果以及提供對(duì)應(yīng)的服務(wù)。而且有些功能又可以細(xì)分為更多的小功能。對(duì)于開(kāi)發(fā)者來(lái)說(shuō)功能是一種應(yīng)用縱向的切分。開(kāi)發(fā)者更喜歡將他說(shuō)成為模塊單元或者說(shuō)是功能。每個(gè)功能能夠提供一個(gè)從界面到業(yè)務(wù)邏輯的完整單元,而且功能之間一般都比較獨(dú)立,功能之間通常通過(guò)接口來(lái)進(jìn)行交互。這樣設(shè)計(jì)的好處是有利于降低系統(tǒng)內(nèi)模塊之間的依賴耦合性,也有利于程序員之間的分工合作和任務(wù)劃分。因此無(wú)論從使用者還是開(kāi)發(fā)者的角度來(lái)看功能劃分都是一種非常好的應(yīng)用程序構(gòu)造方式。功能的展現(xiàn)在設(shè)計(jì)上我們可以理解為通過(guò)視圖來(lái)完成,而業(yè)務(wù)邏輯實(shí)現(xiàn)則是由模型層來(lái)完成,所以必須要存在一個(gè)實(shí)體來(lái)將這兩者關(guān)聯(lián)起來(lái),并且起到統(tǒng)籌和控制的能力。這個(gè)實(shí)體由控制層的控制器來(lái)實(shí)現(xiàn)和擔(dān)當(dāng)最合適。因此在實(shí)踐中我們對(duì)功能的實(shí)現(xiàn)和劃分也通常是以控制器為單位來(lái)構(gòu)建的,控制器是工作在控制層。也就是說(shuō)我們?cè)趯?shí)現(xiàn)某個(gè)功能時(shí)通常是為這個(gè)功能建立一個(gè)對(duì)應(yīng)的控制器來(lái)實(shí)現(xiàn)的,控制器負(fù)責(zé)視圖的構(gòu)建和業(yè)務(wù)模型的調(diào)用,而思想下的框架就是經(jīng)典的MVC框架!
控制層在各平臺(tái)下的實(shí)現(xiàn)
目前主流的iOS和Android移動(dòng)開(kāi)發(fā)平臺(tái)所提供的都是MVC應(yīng)用框架,尤其是對(duì)于控制層的實(shí)現(xiàn)更是幾乎提供了相同的能力和方式。兩個(gè)平臺(tái)的控制層的實(shí)體都是由對(duì)應(yīng)的控制器類(lèi)來(lái)實(shí)現(xiàn)的(iOS叫UIViewController, Android叫Activity)。而且這兩個(gè)平臺(tái)上都提供了控制器的構(gòu)建,視圖的呈現(xiàn)以及到控制器的銷(xiāo)毀的流程方法。這種實(shí)現(xiàn)機(jī)制是一個(gè)非常典型的模板方法設(shè)計(jì)模式,在基類(lèi)中定義了一個(gè)控制器在生命周期內(nèi)各環(huán)節(jié)的調(diào)用方法,您只需要在派生類(lèi)中重載這些方法來(lái)完成控制器生命周期內(nèi)各環(huán)節(jié)所要完成的動(dòng)作或者處理的事情。為了處理控制器之間的交互或者調(diào)用,系統(tǒng)提供了一個(gè)導(dǎo)航棧的管理類(lèi)。導(dǎo)航棧負(fù)責(zé)各功能控制器的進(jìn)入和退出,同時(shí)管理著所有的控制器。
相對(duì)于iOS的UIViewController來(lái)說(shuō)Android的Activity其實(shí)對(duì)功能封裝得更加徹底。Activity具有跨越進(jìn)程的調(diào)用能力,因此作為組件化的能力更加強(qiáng)大,同時(shí)控制器和控制器之間的耦合度也非常得低。對(duì)于控制器之間的參數(shù)傳遞都是通過(guò)序列化和反序列化來(lái)實(shí)現(xiàn)的。但是這也在某方面成為了一個(gè)缺點(diǎn),為了解決這個(gè)問(wèn)題,Android系統(tǒng)中又提供了一個(gè)叫Fragment類(lèi),這是一個(gè)較Activity而言的輕量級(jí)控制器,目的是為了解決某些大功能需要拆解為多個(gè)子功能來(lái)實(shí)現(xiàn)的問(wèn)題以及解決功能之間的參數(shù)傳遞的問(wèn)題。
iOS視圖控制器生命周期的介紹。
前面大體介紹了控制層中控制器的實(shí)現(xiàn)以及控制器的生命周期,同時(shí)也介紹了功能和控制器之間的對(duì)應(yīng)關(guān)系,控制器是視圖和業(yè)務(wù)模型之間聯(lián)系的紐帶,因此控制器必須要在生命周期內(nèi)負(fù)責(zé)視圖的構(gòu)建、管理視圖的呈現(xiàn)、處理用戶的操作、以及進(jìn)行業(yè)務(wù)模型的調(diào)用等工作。為了實(shí)現(xiàn)這些能力,控制器中采用了一種模板方法的設(shè)計(jì)模式來(lái)解決這個(gè)問(wèn)題。這里面我主要想介紹一下iOS視圖控制器為解決這些問(wèn)題而所做的實(shí)現(xiàn)。我們知道iOS中的視圖控制器是叫UIViewController。在這個(gè)類(lèi)中定義了很多的方法來(lái)描述控制器所處的狀態(tài),而每個(gè)從視圖控制器派生的類(lèi)都可以重載對(duì)應(yīng)的方法以便在視圖控制器的相應(yīng)狀態(tài)下進(jìn)行邏輯的處理。下面列出了常見(jiàn)的幾種狀態(tài)下的方法以及這種狀態(tài)下我們通常應(yīng)該要做的事情:
init
這個(gè)是控制器的構(gòu)造方法loadView
在這個(gè)方法中完成視圖的構(gòu)造。如果你的視圖是由Storyboard或者XIB來(lái)構(gòu)建那么你不需要重載這個(gè)方法,但是如果你的視圖是通過(guò)代碼構(gòu)建的話則應(yīng)該重載這個(gè)方法。控制器的默認(rèn)實(shí)現(xiàn)將會(huì)找到關(guān)聯(lián)的Storyboard或者XIB中的視圖布局描述信息, 如果找到了則根據(jù)布局描述來(lái)構(gòu)建要呈現(xiàn)的視圖,如果沒(méi)有找到則會(huì)構(gòu)建出一個(gè)默認(rèn)的空視圖。viewDidLoad
這個(gè)方法被調(diào)用時(shí)表示視圖已經(jīng)構(gòu)建完畢了,一般在這里構(gòu)建模型層的業(yè)務(wù)模型對(duì)象,以及一些事件的綁定,委托delegate的設(shè)置等工作。如果你是通過(guò)代碼來(lái)構(gòu)建布局時(shí),不建議在這里進(jìn)行視圖布局的構(gòu)建而應(yīng)該將構(gòu)建的代碼寫(xiě)在loadView里面去。viewWillAppear
視圖將要呈現(xiàn)時(shí)調(diào)用,只有當(dāng)將一個(gè)視圖添加到一個(gè)窗口UIWindow時(shí)視圖才會(huì)呈現(xiàn)出來(lái),因此這個(gè)方法是在將視圖添加到窗口前被調(diào)用。viewDidAppear
視圖已經(jīng)呈現(xiàn)到窗口中,這個(gè)方法會(huì)在視圖添加到窗口后被調(diào)用。viewWillDisappear
視圖將要從窗口中刪除時(shí)被調(diào)用。viewDidDisappear
視圖已經(jīng)從窗口中刪除時(shí)調(diào)用。dealloc
控制器被銷(xiāo)毀前被調(diào)用。
如何構(gòu)建您的控制層
如何構(gòu)建一個(gè)控制層是一個(gè)非常廣泛的命題,需要具體業(yè)務(wù)具體分析。雖然如此總是還能找到一些共同點(diǎn)和方法論,一個(gè)優(yōu)秀的設(shè)計(jì)方法,將不會(huì)出現(xiàn)所謂的控制器代碼膨脹的問(wèn)題。MVC本身的框架思想非常的優(yōu)秀,當(dāng)出現(xiàn)問(wèn)題時(shí)首先要考慮的并不是去替換掉現(xiàn)有的框架而是從設(shè)計(jì)的角度去優(yōu)化現(xiàn)有的代碼以及邏輯,讓整個(gè)系統(tǒng)達(dá)到一個(gè)最優(yōu)的組合。
1. 功能文件夾的劃分
在前面的論述中可以看出視圖控制器是功能實(shí)現(xiàn)的基本單元,一個(gè)視圖控制器是一個(gè)功能的載體。一個(gè)應(yīng)用中具有多個(gè)功能,而一些相似的功能通常組成一個(gè)功能集,比如一個(gè)應(yīng)用的注冊(cè)流程可能會(huì)分為好幾步;比如說(shuō)用戶體系的各種特性的設(shè)置;比如說(shuō)一個(gè)訂單的支付部分等等。為了對(duì)功能集進(jìn)行管理,可以將某些功能集下的所有功能放置到一個(gè)特定目錄中。最終的構(gòu)成一個(gè)應(yīng)用功能目錄樹(shù):
通過(guò)對(duì)功能進(jìn)行劃分管理,有利于功能的檢索和增強(qiáng)你應(yīng)用系統(tǒng)的可理解性。操作系統(tǒng)以及XCODE上的文件夾就是一種非常常見(jiàn)的功能樹(shù)目錄構(gòu)建方式。在進(jìn)行功能目錄樹(shù)劃分時(shí)注意如下幾個(gè)要點(diǎn)。
- 如果某些功能是一些基本的功能,可能多個(gè)其他功能都會(huì)用到那么可以將這些功能提煉出來(lái)保存到一個(gè)特定的文件夾中(文件夾可以命名為Common或者Base之類(lèi)的)。比如你可以在系統(tǒng)提供的控制器的基礎(chǔ)上派生出你自己的控制器基類(lèi),然后把這些基類(lèi)也可以單獨(dú)的保存到一個(gè)文件夾中。
- 最好不要以每個(gè)功能單獨(dú)建立文件夾來(lái)管理。有些案例里面會(huì)出現(xiàn)每個(gè)VC的.h和.m文件都給他建立文件夾,其實(shí)這樣不可取,因?yàn)橛锌赡軐?dǎo)致文件夾過(guò)多而變得查找定位更加麻煩。我們應(yīng)該以相似的功能聚集在一起來(lái)建立文件夾進(jìn)行管理。
- 有時(shí)候某個(gè)功能集可能過(guò)于龐大,這時(shí)候我們可以對(duì)功能集進(jìn)行再次分類(lèi),并建立子文件夾進(jìn)行管理,文件夾劃分不一定是單層樹(shù)形結(jié)構(gòu)也可以是多層樹(shù)形結(jié)構(gòu)。
- 在XCODE中可以建立兩種文件夾:真實(shí)文件夾(New Group with Folder)和虛擬文件夾(New Group)。 這里建議是最好建立虛擬的文件夾,原因是為了后續(xù)好管理,因?yàn)橛袝r(shí)候可能出現(xiàn)控制器文件從一個(gè)文件夾移動(dòng)到另外一個(gè)文件夾的情況(功能轉(zhuǎn)移)。如果你建立真實(shí)的文件夾的話,那么移動(dòng)后控制器所在的真實(shí)的文件夾就有可能會(huì)和你項(xiàng)目工程上的所在的文件夾對(duì)應(yīng)不上的情況。而用虛擬文件夾就不會(huì)出現(xiàn)這種情況的發(fā)生。
- 功能文件夾的劃分方法有很多種,你可以從業(yè)務(wù)的角度來(lái)劃分文件夾,也可以從你的應(yīng)用界面上的展現(xiàn)來(lái)劃分文件夾。比如一個(gè)應(yīng)用中我們有商品展示體系、支付體系、用戶體系,而我們的界面展示可能是底部分為首頁(yè)、購(gòu)物車(chē),我的組成的四個(gè)Tab界面。這時(shí)候你可以按業(yè)務(wù)來(lái)分為商品、支付、用戶文件夾,也可以按展示界面來(lái)分為首頁(yè)、購(gòu)物車(chē)、我的文件夾。因此文件夾的劃分并沒(méi)有標(biāo)準(zhǔn)就看你個(gè)人的喜好而定了。唯一的要求就是同一個(gè)文件夾內(nèi)的功能要體現(xiàn)出聚合性強(qiáng)的原則,也就是在某一天甚至可以將這部分單獨(dú)抽離出來(lái)構(gòu)建一個(gè)子項(xiàng)目時(shí)而不需要進(jìn)行進(jìn)行大量的改變。
2. 基本控制器以及派生類(lèi)。
一個(gè)應(yīng)用總是會(huì)有自己獨(dú)特的設(shè)計(jì)風(fēng)格,比如標(biāo)題欄的文字和字體以及背景。或者我們要對(duì)應(yīng)用進(jìn)行整體的監(jiān)控,比如對(duì)界面進(jìn)入退出進(jìn)行埋點(diǎn)處理。因此我們需要在系統(tǒng)提供的基本控制器UIViewController, UITableviewController, UINavigationController, UICollectionViewController等控制器之上進(jìn)行派生類(lèi)的構(gòu)建,也就是實(shí)現(xiàn)某個(gè)具體功能的控制器不要從系統(tǒng)的控制器之上派生而應(yīng)該從派生的控制器基類(lèi)之上再派生出來(lái)。這樣我們就可以在我們派生的控制器基類(lèi)上增加一些具有自己特色的業(yè)務(wù)邏輯或者界面邏輯,也可以實(shí)現(xiàn)某些AOP方面的處理。比如我們可以構(gòu)建一個(gè)UINavigationController的派生基類(lèi),這樣在進(jìn)行控制器的push以及pop之前或者之后進(jìn)行一些特殊處理。 但是這樣問(wèn)題就來(lái)了,因?yàn)镺C語(yǔ)言并不支持多重繼承。而我們可能要建立上面四個(gè)系統(tǒng)控制器的派生類(lèi),并且需要在相似的地方添加同樣的代碼,比如都要在viewDidLoad中添加一段相似的代碼。為了實(shí)現(xiàn)這一點(diǎn),就需要添加四份相同的代碼比如:
@interface XXXBaseViewController:UIViewController
@end
@implementation XXXBaseViewController
-(void)helperfn
{
//..實(shí)現(xiàn)特定的擴(kuò)展功能。
}
-(void) viewDidLoad
{
[super viewDidLoad];
[self helperfn]; //調(diào)用擴(kuò)展方法
}
@end
@interface XXXBaseNavigationController: UINavigationController
@end
@implementation XXXBaseNavigationController
-(void)helperfn
{
//..實(shí)現(xiàn)特定的擴(kuò)展功能。
}
-(void) viewDidLoad
{
[super viewDidLoad];
[self helperfn]; //調(diào)用擴(kuò)展方法
}
@end
//...分別實(shí)現(xiàn)的UITableviewController、UICollectionViewController等等都將實(shí)現(xiàn)重復(fù)的代碼。
很明顯這是一種低效率的解決方案,因?yàn)橐坏┬枨笞兏覀兙涂赡苄枰獙?duì)helperfn方法改動(dòng)四次。怎么解決這種問(wèn)題呢?我們分為2種場(chǎng)景:
一、 所有的功能擴(kuò)展中都不需要擴(kuò)展屬性
在這種情況下,因?yàn)閿U(kuò)展的方法中都不需要用到對(duì)象的實(shí)例屬性,所以我們可以通過(guò)建立分類(lèi)(Category)的方法來(lái)實(shí)現(xiàn)這些共有的功能,我們可以為UIViewController建立出一個(gè)分類(lèi)來(lái),并在這個(gè)分類(lèi)中實(shí)現(xiàn)共有的方法,然后在每個(gè)派生類(lèi)的特定位置中調(diào)用這個(gè)共享的分類(lèi)方法。具體代碼如下:
@interface UIViewController(XXXExtend)
-(void)helperfn;
@end
@implementation UIViewController(XXXExtend)
//一份實(shí)現(xiàn)
-(void)helperfn
{
//實(shí)現(xiàn)特定功能。
}
@end
@interface XXXBaseViewController:UIViewController
@end
@implementation XXXBaseViewController
-(void) viewDidLoad
{
[super viewDidLoad];
[self helperfn]; //調(diào)用擴(kuò)展方法
}
@end
@interface XXXBaseNavigationController: UINavigationController
@end
@implementation XXXBaseNavigationController
-(void) viewDidLoad
{
[super viewDidLoad];
[self helperfn]; //調(diào)用擴(kuò)展方法
}
@end
//...分別實(shí)現(xiàn)的UITableviewController、UICollectionViewController的派生類(lèi)。
當(dāng)然你也許覺(jué)得上面的方法需要在每個(gè)派生類(lèi)的特定地點(diǎn)都添加一遍代碼而感到麻煩,你也可以采用method swizzle的方法來(lái)解決上述的問(wèn)題,你可以在分類(lèi)的+load方法中實(shí)現(xiàn)代碼替換。下面是具體的代碼實(shí)現(xiàn):
@interface UIViewController(XXXExtend)
@end
@implementation UIViewController(XXXExtend)
-(void)helperfn
{
}
-(void)viewDidLoadXXXExtend
{
[self viewDidLoadXXXExtend];
[self helperfn];
}
+(void)load
{
Method originalMethod = class_getInstanceMethod(self, @selector(viewDidLoad));
Method swizzledMethod = class_getInstanceMethod(self, @selector(viewDidLoadXXXExtend));
method_exchangeImplementations(originalMethod, swizzledMethod);
}
@end
@interface XXXBaseViewController:UIViewController
@end
@implementation XXXBaseViewController
@end
@interface XXXBaseNavigationController: UINavigationController
@end
@implementation XXXBaseNavigationController
@end
//...分別實(shí)現(xiàn)的UITableviewController、UICollectionViewController的派生類(lèi)。
二、有需要擴(kuò)展屬性的情況。
如果你的基類(lèi)擴(kuò)展方法中有用到屬性的話那么我們知道分類(lèi)中是不能支持編譯時(shí)擴(kuò)展屬性的(但是支持運(yùn)行時(shí)擴(kuò)展屬性的增加)。除了用運(yùn)算時(shí)擴(kuò)展屬性的方法外,還可以將共有的方法和屬性單獨(dú)提煉出來(lái)讓一個(gè)輔助類(lèi)來(lái)實(shí)現(xiàn),然后在派生基類(lèi)的初始化方法中創(chuàng)建這個(gè)輔助類(lèi),并且后續(xù)的一些方法都委托給輔助類(lèi)來(lái)實(shí)現(xiàn)。具體的結(jié)構(gòu)設(shè)計(jì)如下:
//Helper.h
@interface Helper:NSObject
@property id prop1;
@property id prop2;
-(void)fn1;
-(void)fn2;
-(id)initWithViewController:(UIViewController*)vc;
@end
//Helper.m
@interface Helper()
@property(weak) UIViewController *vc; //這里一定要定義為弱引用
@end
@implementation Helper
-(id)initWithViewController:(UIViewController*)vc
{
self = [self init];
if (self != nil)
{
_vc = vc;
}
return self;
}
-(void)fn1
{
//...
self.vc.xxxx; //這里可以訪問(wèn)視圖控制器的方法。
}
@end
..........................................
//XXXBaseViewController.h
@interface XXXBaseViewController:UIViewController
@property id prop1;
@end
//XXXBaseViewController.m
@interface XXXBaseViewController()
@property(strong) Helper *helper;
@end
@implementation XXXBaseViewController
-(id)init
{
self = [super init];
if (self != nil)
{
//在視圖控制器的初始化里面初始化一個(gè)幫助對(duì)象。
_helper = [[Helper alloc] initWithViewController:self];
}
return self;
}
//重寫(xiě)控制器中的屬性,并把真實(shí)的實(shí)現(xiàn)委托給helper來(lái)完成
-(void)setProp1:(id)prop1
{
self.helper.prop1 = prop1;
}
-(id)prop1
{
return self.helper.prop1;
}
-(void) viewDidLoad
{
[super viewDidLoad];
[self.helper fn1]; //調(diào)用幫助類(lèi)的方法。
}
@end
//在你的其他幾個(gè)派生類(lèi)中采用同樣的機(jī)制來(lái)處理。
從上面可以看出來(lái),輔助類(lèi)里面設(shè)計(jì)了一個(gè)弱引用指針來(lái)指向控制器,而控制器則是強(qiáng)引用輔助類(lèi),這樣做的目的是為了防止循環(huán)引用的發(fā)生,而且這種設(shè)計(jì)模式也是一種在實(shí)踐中非常經(jīng)典的方法:有時(shí)候我們需要將類(lèi)A的某些功能委托給類(lèi)B實(shí)現(xiàn),而B(niǎo)又有可能會(huì)在特定的地方訪問(wèn)A的屬性,為了防止相互引用而形成死鎖導(dǎo)致兩個(gè)對(duì)象都無(wú)法被釋放,這時(shí)候就需要使用強(qiáng)弱引用來(lái)解決這個(gè)問(wèn)題。上面借助輔助類(lèi)來(lái)實(shí)現(xiàn)的方法可以解決我們的派生類(lèi)中代碼重復(fù)的問(wèn)題。上面的方法缺點(diǎn)就是我們的派生類(lèi)中需要編寫(xiě)很多重復(fù)的、程式化的代碼。如何來(lái)精簡(jiǎn)呢?其實(shí)我們可以借助接口協(xié)議和OC中的forwarding機(jī)制來(lái)解決這些問(wèn)題:
//Helper.h
@protocol Helper
//請(qǐng)將接口的屬性和方法都設(shè)置為可選
@optional
@property id prop1;
@property id prop2;
-(void)fn1;
-(void)fn2;
@end
//真實(shí)實(shí)現(xiàn)接口的對(duì)象
@interface Helper:NSObject<Helper>
@property id prop1;
@property id prop2;
-(id)initWithViewController:(UIViewController*)vc;
@end
//Helper.m
@interface Helper()
@property(weak) UIViewController *vc; //這里一定要定義為弱引用
@end
@implementation Helper
-(id)initWithViewController:(UIViewController*)vc
{
self = [self init];
if (self != nil)
{
_vc = vc;
}
return self;
}
-(void)fn1
{
//...
self.vc.xxxx; //這里可以訪問(wèn)視圖控制器的方法。
}
-(void)fn2
{
//...
}
@end
..........................................
//XXXBaseViewController.h
@interface XXXBaseViewController:UIViewController
@end
//XXXBaseViewController.m
//這里實(shí)現(xiàn)Helper協(xié)議,如果基類(lèi)的擴(kuò)展屬性可以被外面訪問(wèn)則應(yīng)該在.h中的類(lèi)申明里面表明實(shí)現(xiàn)了Helper協(xié)議
@interface XXXBaseViewController()<Helper>
@property(strong) Helper *helper;
@end
@implementation XXXBaseViewController
-(id)init
{
self = [super init];
if (self != nil)
{
//在視圖控制器的初始化里面初始化一個(gè)幫助對(duì)象。
_helper = [[Helper alloc] initWithViewController:self];
}
return self;
}
-(void) viewDidLoad
{
[super viewDidLoad];
[self fn1]; //調(diào)用fn1
id *p = self.prop1 //讀取屬性。
}
//這是實(shí)現(xiàn)的關(guān)鍵點(diǎn),重載這個(gè)方法。
-(id)forwardingTargetForSelector:(SEL)aSelector
{
//判斷這個(gè)方法是否是協(xié)議定義的方法,如果是則將方法的實(shí)現(xiàn)者設(shè)置為helper對(duì)象。
struct objc_method_description omd = protocol_getMethodDescription(@protocol(Helper), aSelector, NO, YES);
if (omd.name != NULL)
{
return self.helper;
}
return [super forwardingTargetForSelector:aSelector];
}
@end
//在你的其他幾個(gè)派生類(lèi)中采用同樣的機(jī)制來(lái)處理。
可以看出我們可以借助OC的forwardingTargetForSelector方法來(lái)實(shí)現(xiàn)方法調(diào)用的轉(zhuǎn)發(fā)處理,而不必具體的定義方法的實(shí)現(xiàn)。
3.對(duì)內(nèi)和對(duì)外的接口定義
面向?qū)ο缶幊痰囊粋€(gè)主旨思想就是封裝,所謂封裝就是在進(jìn)行類(lèi)的定義和設(shè)計(jì)時(shí),我們盡可能的暴露出外面可以理解以及需要的接口或者方法,而在內(nèi)部實(shí)現(xiàn)中所用到的中間特性或者私有特性則盡可能的隱藏。盡可能的隱藏復(fù)雜的實(shí)現(xiàn)細(xì)節(jié),而把簡(jiǎn)單易用的接口暴露給外部使用。比如對(duì)于汽車(chē)的封裝,我們對(duì)外暴露的就僅僅是開(kāi)鎖、發(fā)動(dòng)、掛擋、轉(zhuǎn)向等操作,而具體內(nèi)部的組成、發(fā)動(dòng)機(jī)的原理、以及發(fā)動(dòng)機(jī)如何驅(qū)動(dòng)行走等細(xì)節(jié)都不需要開(kāi)車(chē)的人了解。正是面向?qū)ο筮@種封裝的特性就使得我們能更加從應(yīng)用層面去使用某個(gè)對(duì)象的方法而不需要知道其中的細(xì)節(jié)。因此我們?cè)陬?lèi)的設(shè)計(jì)中也要遵循這個(gè)設(shè)計(jì)的思想,把必要的東西暴露給外部,而把實(shí)現(xiàn)細(xì)節(jié)則隱藏在類(lèi)的內(nèi)部來(lái)完成。這一節(jié)所介紹的并不僅僅適用在控制器類(lèi)的設(shè)計(jì)上,所有其他系統(tǒng)也是同樣適用的。
類(lèi)的封裝實(shí)現(xiàn)在不同的語(yǔ)言上所提供的能力是不一樣的,這一點(diǎn)非常有意思。向在C/C++/OC這幾種語(yǔ)言中,類(lèi)的聲明和類(lèi)的實(shí)現(xiàn)需要在不同的文件里面完成(.h是聲明,而.m/.c/.cpp中則是實(shí)現(xiàn))而像Java和Swift等語(yǔ)言則是申明和實(shí)現(xiàn)都放在同一文件中完成。在前面的三種語(yǔ)言中因?yàn)槁暶骱蛯?shí)現(xiàn)分離,所以我們可以把一些對(duì)外暴露的方法和屬性放到頭文件中申明,而內(nèi)部的私有屬性則放到實(shí)現(xiàn)文件中申明和定義。而使用者則只需要引入共有頭文件即可。而后面兩種語(yǔ)言中因?yàn)闆](méi)有分開(kāi),所以在這些語(yǔ)言更傾向于通過(guò)接口定義和實(shí)現(xiàn)來(lái)完成這種共有屬性和私有屬性分類(lèi)的機(jī)制(您可以看出在Java中大量的使用了接口來(lái)完成整個(gè)體系架構(gòu),以及Swift中也是推崇接口編程這種理念)。
面向?qū)ο笤O(shè)計(jì)中,類(lèi)和類(lèi)之間不可能獨(dú)立存在,他們之間總是要建立一種關(guān)聯(lián),這種關(guān)聯(lián)有可能是單向的也有可能是雙向的。我們都推崇類(lèi)和類(lèi)之間的單向依賴來(lái)降低類(lèi)與類(lèi)之間的耦合性。這種單向依賴至少在明面上是如此的,也就是類(lèi)所公開(kāi)的方法和屬性是可以看出來(lái)的。但是在實(shí)際的內(nèi)部實(shí)現(xiàn)中這種單向依賴可以就會(huì)被打破。舉一個(gè)很常見(jiàn)的例子我們都知道視圖控制器UIViewController中有一個(gè)view屬性來(lái)保存控制器所管理的視圖,但是我們?cè)谝晥DUIView中卻看不見(jiàn)任何關(guān)于控制器的屬性。這樣的表象就是表明視圖控制器依賴視圖,而視圖則不依賴視圖控制器,這也是非常符合MVC中三層設(shè)計(jì)思路的。但實(shí)際中是如此嗎? 結(jié)果并不是這樣的,因?yàn)樵谙到y(tǒng)的內(nèi)部如果某個(gè)視圖是控制器的根視圖的話他可能會(huì)具有一些不同的特性以及不同的處理邏輯,因此其實(shí)在UIView的內(nèi)部私有屬性中是有一個(gè)視圖所歸屬的視圖控制器的屬性的,這個(gè)屬性就是:viewDelegate。 并且在UIView上他是定義為了id類(lèi)型的。
上面的兩段描述中我們都提到了對(duì)公和對(duì)私的方法和屬性的申明的問(wèn)題,可以看出在設(shè)計(jì)上要按照這個(gè)思路去設(shè)計(jì)我們的類(lèi),我們只需要將共有的方法和屬性暴露出來(lái),而將私有的方法和屬性則隱藏起來(lái)。那么怎么來(lái)實(shí)現(xiàn)這種共有和私有方法的定義實(shí)現(xiàn)呢?我們來(lái)看下面三個(gè)具體的場(chǎng)景:
- 類(lèi)的某些屬性公開(kāi)某些屬性不公開(kāi)
/*一個(gè)類(lèi)里面某些屬性公開(kāi)某些屬性不公開(kāi)的實(shí)現(xiàn)可以很簡(jiǎn)單的通過(guò)類(lèi)的申明和類(lèi)的擴(kuò)展來(lái)實(shí)現(xiàn)*/
//XXXX.h
@interface XXXX
//對(duì)外公開(kāi)的屬性
@property id pubp1;
@property id pubp2;
//對(duì)外公開(kāi)的方法
-(void)pubfn1;
-(void)pubfn2;
@end
//XXXX.m
//只在內(nèi)部使用的屬性和方法定義在擴(kuò)展中。
@interface XXXX()
//對(duì)內(nèi)私有的屬性
@property id prip1;
@property id prip2;
//對(duì)內(nèi)私有的方法
-(void)prifn1;
-(void)prifn2;
@end
@implementation XXXX
@end
- 類(lèi)的某些屬性對(duì)外暴露的是只讀的,但是內(nèi)部實(shí)現(xiàn)確實(shí)可以被改變的。這樣做的目的是為了訪問(wèn)和使用的安全。
//XXXX.h
@interface XXXX
//對(duì)外公開(kāi)的屬性只讀
@property(readonly) id pubp1;
@property(readonly) id pubp2;
@end
//XXXX.m
//只在內(nèi)部使用的屬性和方法定義在擴(kuò)展中。
@interface XXXX()
//在類(lèi)的內(nèi)部這些屬性是可讀寫(xiě)的
@property id pubp1;
@property id pubp1;
@end
@implementation XXXX
@end
- 類(lèi)A和類(lèi)B之間有關(guān)聯(lián),A中持有B的實(shí)例并公開(kāi),而B(niǎo)則有可能在實(shí)現(xiàn)中需要用到A的內(nèi)部的方法或者屬性,同時(shí)B是不向外暴露對(duì)A的持有的情況。因?yàn)槲覀兌际峭ㄟ^(guò)頭文件引用,所以在頭文件中看不到這種互相依賴關(guān)系,但是內(nèi)部的實(shí)現(xiàn)文件則可以很清楚的看到其中的依賴了。
//A.h
@interface A
@property(strong) B *b; //A的外部暴露持有B
@property id others;
-(void) pubfn1;
@end
//A.m
//在A的內(nèi)部用到了B的prip1, prifn1所以要這里申明一下。
@interface B()
@property id prip1;
-(void)prifn1;
@end
//內(nèi)部實(shí)現(xiàn)
@interface A()
@property id prip1;
-(void)prifn1;
@end
@implementation A
-(void)pubfn1
{
//...
[b prifn1]; //A內(nèi)部調(diào)用B的私有方法
[b pubfn1];
id temp = b.prip1; //內(nèi)部屬性
}
@end
...............................
//B.h
//B的外部并不暴露對(duì)A的持有
@interface B
-(void)pubfn1;
@end
//B.m
@interface A()
//因?yàn)锽內(nèi)部要用到prifn1所以這里再申明一下。
-(void)prifn1;
@end
@interface B()
//B的內(nèi)部持有A。這里因?yàn)橛邢嗷コ钟兴幸幸粋€(gè)是強(qiáng)持有一個(gè)是弱持有。
@property(weak) A *a;
//內(nèi)部實(shí)現(xiàn)的其他
@property id prip1;
-(void)prifn1;
@end
@implementation B
-(void)pubfn1
{
[a prifn1];
}
@end
4.各種屬性的定義以及分類(lèi)擴(kuò)展
控制器用來(lái)實(shí)現(xiàn)對(duì)視圖對(duì)象和業(yè)務(wù)模型對(duì)象的建立以及管理和控制,在實(shí)現(xiàn)上控制器會(huì)擁有眾多視圖層對(duì)象的屬性,以及模型層對(duì)象的屬性,同時(shí)還會(huì)擁有自身的一些屬性。同時(shí)控制器還要在適當(dāng)?shù)臅r(shí)候?qū)τ脩舻妮斎脒M(jìn)行處理,以及在適當(dāng)?shù)臅r(shí)候調(diào)用業(yè)務(wù)模型所提供的服務(wù),還要在適當(dāng)?shù)臅r(shí)候?qū)I(yè)務(wù)模型提供服務(wù)的結(jié)果通知給視圖進(jìn)行呈現(xiàn)和更新。因此如何去組織一個(gè)控制器的代碼布局(此代碼布局非視圖的界面布局而是源代碼的布局)就非常的重要了。如何合理的定義以及放置屬性,如何合理的對(duì)控制器中的方法進(jìn)行分類(lèi),以及在何時(shí)創(chuàng)建視圖、在何時(shí)創(chuàng)建業(yè)務(wù)對(duì)象,在何時(shí)添加和銷(xiāo)毀觀察者,在類(lèi)的析構(gòu)中作如何處理等等這些其實(shí)都是有一定的規(guī)則和規(guī)范的。這一節(jié)更像是一份代碼規(guī)約方面的介紹。我將會(huì)從下面幾個(gè)點(diǎn)來(lái)分別闡述。
(一). 屬性的定義順序和規(guī)則
一個(gè)類(lèi)的設(shè)計(jì)首要構(gòu)造的就是屬性和成員變量,控制器也無(wú)外乎。前面說(shuō)到控制器管理著視圖對(duì)象和模型對(duì)象,因此我們一般要將視圖對(duì)象和業(yè)務(wù)對(duì)象作為屬性定義在控制器中。這里整理出一下幾點(diǎn):
- 如果控制器中的屬性和成員變量只在類(lèi)內(nèi)部使用和訪問(wèn),那么我們應(yīng)該要將屬性定義在控制器的實(shí)現(xiàn)文件中的擴(kuò)展里面,而不要定義在控制器的頭文件中,除非這個(gè)屬性會(huì)被外部訪問(wèn)或者設(shè)置。比如如下代碼:
//XXX.m
@interface XXX()
//在擴(kuò)展中定義內(nèi)部使用的屬性
@property(nonatomic, weak) UILabel *label;
@property(nonatomic, strong) User *user;
@end
@implement XXX
@end
如果控制器中需要訪問(wèn)某個(gè)子控件視圖那么在定義子控件視圖時(shí),屬性最好是weak而非strong。這樣做的目的一來(lái)iOS對(duì)于SB或者XIB上的子控件的屬性定義都是默認(rèn)為weak的、二來(lái)最主要的原因是有可能控制器中的根視圖有可能會(huì)在運(yùn)行時(shí)被重新構(gòu)造(比如說(shuō)我們要實(shí)現(xiàn)一個(gè)換膚功能,我們就有可能會(huì)重新構(gòu)造視圖控制器中的根視圖來(lái)實(shí)現(xiàn))這樣當(dāng)控制器中的根視圖被銷(xiāo)毀時(shí),根視圖里面的子視圖也應(yīng)該被銷(xiāo)毀,而如果你用strong來(lái)定義子視圖時(shí)就有可能導(dǎo)致子視圖的生命周期要長(zhǎng)于根視圖。另外有可能我們的子控件會(huì)采用懶加載的模式來(lái)實(shí)現(xiàn)根視圖中子視圖的建立,因此如果你用strong的話就有可能導(dǎo)致子視圖不會(huì)被重新構(gòu)建。
對(duì)于NSString類(lèi)型的屬性來(lái)說(shuō)我們最好將他聲明為copy。原因是如果聲明為strong或者assign的話,那么對(duì)于NSMutableString類(lèi)型的字符串進(jìn)行賦值時(shí)就有可能會(huì)在后續(xù)的代碼中內(nèi)容被改變,從而引起異常的問(wèn)題。比如:
NSMutableString *str1 = [@"hello" mutableCopy];
self.userName = str1; //如果userName被申明為strong的話則后續(xù)對(duì)str1的內(nèi)容的更改同時(shí)也會(huì)導(dǎo)致userName的內(nèi)容的改變!!
如果你的屬性不會(huì)涉及到任何多線程訪問(wèn)的場(chǎng)景那么最好不要在屬性定義上帶上atomic 修飾符。原因是如果帶上atomic修飾符的話所有屬性的賦值和讀取操作都會(huì)通過(guò)操作系統(tǒng)原子API來(lái)進(jìn)行賦值和讀取。
不要將狀態(tài)以及持久數(shù)據(jù)保存到視圖對(duì)象中。
如果可能最好將控制器中的視圖對(duì)象屬性和模型對(duì)象屬性分開(kāi)定義,并且把視圖對(duì)象屬性放在最上面, 控制器本地的屬性放在中間,而模型對(duì)象屬性放在最下面。
下面是一個(gè)典型的控制器屬性定義的代碼示例,僅供大家參考:
//XXXViewController.m
@interface XXXViewController()
//不同層次上的屬性分開(kāi)定義
//視圖對(duì)象屬性放在最上面。
@property(nonatomic, weak) UITextField *nameLabel;
@property(nonatomic, weak) UITextField *passwordLabel;
//中間定義控制器本身的變量
@property(nonatomic, copy) NSString *name;
@property(nonatomic, assign) BOOL isAgree;
//底部定義模型對(duì)象屬性
@property(nonatomic, strong) User *user;
@end
@implement XXXViewController
@end
(二). 類(lèi)中各種方法的分類(lèi)
當(dāng)你的屬性定義完畢后,我們就要實(shí)現(xiàn)方法了。在一個(gè)類(lèi)的方法中我們有構(gòu)造和析構(gòu)的方法、有需要重載的方法、有事件處理的方法、有個(gè)委托Delegate或者觀察者的方法、還有一些對(duì)外公開(kāi)的方法、以及一些私有輔助的方法。為了便于管理,我們最好能將這些方法進(jìn)行分類(lèi)擺放,這樣也有利于查找定位,對(duì)于一個(gè)控制器來(lái)說(shuō)一般就是上面所說(shuō)的幾種方法,一般情況下我們對(duì)相同性質(zhì)的方法放在一塊實(shí)現(xiàn),并用一些特定的關(guān)鍵字或者用分類(lèi)的機(jī)制來(lái)對(duì)控制器中的所有方法進(jìn)行歸類(lèi)。下面我用兩種不同的方式來(lái)對(duì)方法進(jìn)行歸類(lèi)處理:
- 通過(guò)語(yǔ)法關(guān)鍵字。
在OC中我們可以通過(guò) #progma mark -- 名稱(chēng) 來(lái)便于定位和查找。在實(shí)踐中控制器類(lèi)一般都要實(shí)現(xiàn):重寫(xiě)基類(lèi)的方法、公有方法、事件處理方法、Delegate中的方法、私有方法這幾種類(lèi)型,因此我們可以專(zhuān)門(mén)為這些方法定義不同的標(biāo)簽。具體如下:
// XXXViewController.m
@implement XXXViewController
//最開(kāi)始放構(gòu)造和析構(gòu)的方法。這兩個(gè)方法放在一起就可以清楚的看出我初始化了那些東西,析構(gòu)了那些東西。
-(instanceType)initXXXXX{}
-(void)dealloc{}
//接下來(lái)放重載的方法。對(duì)屬性set,get重寫(xiě)的方法
-(void)viewDidLoad{}
-(void)viewWillAppear:(BOOL)animated{}
-(void)viewDidAppear:(BOOL)animated{}
-(NSString*)name{}
-(void)setName:(NSString*)name{}
//接下來(lái)放共有的方法,也就是控制器類(lèi)暴露給外面調(diào)用的方法。
#pragma mark -- Public Methods
-(void)publicFn1{}
-(void)publicFn2{}
//接下來(lái)放事件處理的方法,這些事件處理包括控件的觸摸事件,NSNotificationCenter、定時(shí)器事件、其他的注冊(cè)的事件。我個(gè)人比較喜歡以handle開(kāi)頭。
#pragma mark -- Handle Methods
-(void)handleAction1:(id)sender{}
-(void)handleAction2:(NSNotification*)noti{}
-(void)handleAction3:(NSTimer*)timer{}
//接下來(lái)是放各種Delegate的事件,我們名稱(chēng)都以Delegate協(xié)議的名稱(chēng)做標(biāo)志。
#pragma mark -- UIDataSource
//..這里添加代理的方法。。。
#pragma mark -- UITableViewDelegate
//接下來(lái)是存放各種私有方法。
#pragma mark -- Private Methods
-(void)privateFn1{}
-(void)privateFn2{}
@end
- 通過(guò)分類(lèi)
如果你不想使用#pragma 帶標(biāo)簽的形式,那也可以用分類(lèi)的方式來(lái)實(shí)現(xiàn)。比如下面的例子:
// XXXViewController.m
@implement XXXViewController
//默認(rèn)的部分實(shí)現(xiàn),構(gòu)造和析構(gòu)方法以及所有從基類(lèi)重載的方法
-(instanceType)initXXXXX{}
-(void)dealloc{}
//接下來(lái)放重載的方法。對(duì)屬性set,get重寫(xiě)的方法
-(void)viewDidLoad{}
-(void)viewWillAppear:(BOOL)animated{}
-(void)viewDidAppear:(BOOL)animated{}
-(NSString*)name{}
-(void)setName:(NSString*)name{}
@end
//公有方法
@implement XXXViewController(Public)
@end
//事件處理方法
@implement XXXViewController(Handle)
@end
@implement XXXViewController(UIDataSource)
@end
@implement XXXViewController(UITableViewDelegate)
@end
@implement XXXViewController(Private)
@end
請(qǐng)閱讀:MVC框架之控制層的構(gòu)建(下)
歡迎大家訪問(wèn)我的github地址和簡(jiǎn)書(shū)地址