天天看點

《iOS應用開發》——2.4節重要的設計模式

本節書摘來自異步社群《ios應用開發》一書中的第2章,第2.4節重要的設計模式,作者【美】richard warren,更多章節内容可以通路雲栖社群“異步社群”公衆号檢視

2.4 重要的設計模式

ios應用開發

雖然我們已經掌握了objective-c的大部分基本特征,不過ios sdk中還使用了一些常見的設計模式。花一點時間重溫這些設計模式是很值得的,當你看到它們的時候就可以更好地了解它們。

2.4.1 模型-視圖-控制器

模型-視圖-控制器(mvc)是使用圖形使用者界面建立應用程式時常見的一種架構模式。這個模式将應用劃分為3個部分。模型維護應用的狀态。通常來說,它不僅管理應用程式在運作時的狀态,也包括存儲和加載狀态(即将對象儲存至檔案,将資料儲存至一個sql資料庫,或者是使用類似core data的架構來管理對象資料)。

正如其名,視圖就是以互動的方式來顯示應用程式資訊,大多數情況下是指通過gui顯示資訊,然而,除此之外視圖還包含了列印文檔并且生成其他日志和報告。

控制器存在于模型和視圖之間,它響應事件(通常來自于視圖)并将指令發送至模型(以改變它的狀态)。同樣,當模型發生改變時,它也會更新相應的視圖。按照應用的需求,它的業務邏輯有可能在控制器中實作,也有可能在模型中實作。無論如何,在應用開發的整個過程中,你應該選擇一種方式并保持一緻性。

理想情況下,mvc的各部分元件應是非常松散地連結在一起。例如,任何多數量的視圖都可以觀察控制器,觸發控制器事件并響應任何變化的通知。無論是在gui上顯示資訊還是在寫日志檔案,控制器不會知道或者關心這些細節。而另一方面,模型和控制器的也應該是類似松散地連接配接在一起。

在實踐中,不同層級往往是更緊密地結合,為了實用性犧牲了一點理想主義。cocoa touch也不另外。通常,視圖與控制器連結得更加緊密。每個場景都有量身定制的控制器來專門管理(關于場景和視圖控制器的更多資訊,參見第1章中的“檢查storyboard”一節)。在模型這一端,則有更多的回旋餘地。在最簡單的應用中,模型将會直接在控制器中實作。但是大部分情況下,我們還是盡可能将它們獨立開來。

cocoa touch使用uiview子類來實作視圖,用uiviewcontroller子類來實作控制器。模型和視圖之間的通信通常使用目标/操作的連接配接。

就模型而言,我們可以使用許多不同的技術來實作模型:自定義類、sqlite或者core data。在很多情況下,模型和控制器直接和彼此通信,雖然我們可以使用通知和委托建立更加松散的連接配接(參見下一節讨論資料源以獲得更多的資訊)。

2.4.2 委托

委托讓你可以擴充或者修改一個對象的行為而不用建立該對象的子類。甚至你可以通過在運作時切換委托進而改變對象的行為,雖然在實踐中這樣做很罕見。

我們在前面已經接觸過委托。例如,為了避免在每個項目中都為uiapplication類建立子類,我們實作一個自定的uiapplication`` delegate,然後就可以使用通用的uiapplication類了。應用程式委托協定定義了超過20個可選方法,我們可以重載這些方法來監控和改變應用程式的行為。

任何使用委托的類通常都會有一個命名為(毫無意外)delegate的屬性。遵循約定,委托屬性應該永遠使用弱引用。這樣有助于避免保留循環。然而,我們需要確定在應用程式中的其他地方有一個委托對象的強引用,否則arc就會釋放它。

正如這個例子所示,我們通常聲明一個委托必須實作的協定。該協定定義了委托類和我們的委托之間的接口。委托類是這個關系中的主動方。它在委托上調用協定的方法。有的調用是将資訊傳遞給委托,有的則是查詢委托。通過這種方式,使得兩個對象之間可以來回傳遞資訊。

委托方法通常有一個些許刻闆的名字。遵照約定,名字以描述委托對象的辨別打頭。例如,所有uiapplicationdelegate的方法都以application開始。

在很多情況下,方法會向委托對象傳遞一個引用作為它的第一個參數。uitableviewdelegate就是這麼做的。這意味着你可以使用一個uitableviewdelegate來管理多個uitableviews(雖然這麼做很罕見)。更重要的是,你可以避免在一個執行個體變量中存儲委托類,雖然委托總是通過這個變量來通路它的委托類。

此外,委托的方法通常在它們的名字中包含will、did或者should。在這三種情況下,系統調用這些方法來響應某些變化。w<code>ill方法在變化發生之前被調用。d</code>id方法在變化發生之後被調用。s``hould方法和will方法一樣,在變化發生之前被調用,但是它要求傳回一個yes或者no值。如果它傳回了一個yes值,變化就會正常進行。如果傳回了no值,那麼變化就會被取消。

最後,委托方法幾乎總是可選的。結果,委托類應該首先核對,以確定委托在調用方法之前實作了它。下面的代碼模拟了典型的實作:

委托非常容易實作。隻要建立一個新的類并且讓它采用指定的協定。然後實作所有必需的方法(如果有的話),以及所有你感興趣的可選方法。接着在你的代碼中建立一個委托類的執行個體并且将它指派給主對象的delegate屬性。uiappkit的很多視圖子類可以接受委托。這意味着你将經常通過interface builder連接配接視圖和委托(生成@property聲明中的iboutlet标記)。

注意:

objective-c 2.0中引入了可選協定方法。在這之前,大部分委托都是使用非正式的協定實作的。這個約定涉及到在nsobject類上聲明類别,然而,方法并沒有被定義。當它運作時,ide和編譯器不能提供很多支援。随着時間的推移,蘋果公司慢慢用真正的協定代替了非正式的協定,然而,你仍然會偶然遇見它們。

一些視圖子類同樣也需要資料源。資料源和委托是緊密相關的。然而,委托被用來監控和改變對象行為,而資料源是專門用來為對象提供資料的。其他的差別都是由這個差別産生的。例如,委托通常是可選的,委托對象應該在沒有委托的時候也功能完整。而另一方面呢,資料源經常被主類所需要以便實作正确的功能。是以,資料源通常在協定中聲明了一個或者更多的必需方法。

其他方面,資料源的行為和委托很相似。命名約定是相似的,并且和委托一樣,主類應該持有它的資料源的一個弱引用。

2.4.3 通知

通知讓對象不需要緊密耦合就能通信。在ios系統中,使用通知中心來管理通知。想要收到通知的對象必須在通知中心注冊。同時,想要廣播通知的對象隻要把通知發送給通知将中心就可以了。通知中心就會通過發送對象和通知名稱來處理通知,将每個通知發送給正确的接收者。

通知非常容易實作。首先,你通常會在通用的頭檔案中建立一個nsstring常量作為通知的名稱。發送和接收對象都需要通路這個常量。

發送通知則更加簡單。我們的發送對象隻需要使用一個指向預設消息中心的指針,然後投遞所要發送的消息即可。

發送通知是同步操作。這意味着postnotificationname調用要等到通知中心完成調用所有比對的觀察者的指定選擇器後才會傳回。這會花費相當可觀的時間,尤其是在觀察者數量很大或者響應方法很慢時。

另外,我們可以使用nsnotification隊列來發送異步通知。通知隊列實際上會将通知推遲到目前的事件循環結束以後才發送(或者有可能直到事件循環完全空閑了)。隊列還能夠将重複的消息合并為單個通知。

下列的示例代碼将通知延遲到運作循環空閑後才發送:

2.4.4 鍵-值編碼

鍵-值編碼是使用字元串間接擷取和設定對象的執行個體變量的技術。nskeyvaluecoding協定定義了擷取或者設定這些值的一些方法。最簡單的例子就是valueforkey:以及setvalue:forkey``:。

為了能這樣做,你的對象必須相容kvc(鍵-值編碼)。實際上,valueforkey:方法會查找一個名為&lt;key&gt;或者is&lt;key&gt;的祖先。如果它沒能找到一個有效的祖先,它就會查找一個名為&lt;key&gt;或_<code>&amp;lt;key&amp;gt;的執行個體變量。另一方面,setvalue:forkey</code>:方法查找一個set&lt;key&gt;``:方法,然後尋找執行個體變量。

幸運的是,你為執行個體變量定義的任何屬性都是相容kvc(鍵-值編碼)的。

kvc(鍵-值編碼)方法還可以使用關鍵字路徑(key paths)。關鍵字路徑就是用點分隔的關鍵字清單。實際上,擷取器或者通路器将會在整個關鍵字路徑中依次進行下去。第一個關鍵字用于接收對象。随後每個關鍵字都用于之前的關鍵字所傳回的值上。這讓你可以沿着整個對象層級圖通路到你想要的值。

雖然kvc(鍵-值編碼)可以被用來産生高度動态耦合度非常低的代碼,但是它是有點特殊化的技術,你可能從來不會直接使用kvc。然而,在它之上發展了一些有意思的技術(例如鍵-值觀察)。

2.4.5 鍵-值觀察

鍵-值觀察讓一個對象能夠觀察另外一個對象執行個體變量的任何變化。雖然這在表面上和通知很相似,它們還是有一些重要的差別。首先,kvo(鍵-值觀察)沒有中心控制層。一個對象直接觀察另外一個對象。其次,被觀察的對象一般不需要做任何操作來發送這些通知。隻要它們的執行個體變量是相容kvc(鍵-值編碼)的,不論你的應用程式是使用執行個體變量的設定器還是kvc(鍵-值編碼)來改變執行個體變量的值時,都會自動發送通知。

不妙的是,如果你直接修改執行個體變量的值,被觀測的對象必須在變化之前手動調用willchangevalueforkey<code>:方法,在變化之後調用didchangevalueforkey</code>:方法。這又是一個總是應該通過屬性通路執行個體變量值的理由。

為了注冊成為觀察者,調用addobserver:forkeypath:options:<code></code>context:``。觀察者就是将會接收kvo(鍵-值觀察)通知的對象。forkeypath就是由圓點分隔的關鍵字清單,用來指定将會被觀察的值。options參數決定了通知傳回什麼資訊,而context參數讓你可以傳遞添加到通知中的任意資料。

在使用通知中心時,在觀察者釋放之前删除它是非常重要的。雖然nsn``otificationcenter有一個删除所有指定觀察者通知的便利方法,但是在kvo(鍵-值觀察)中,你必須為每一次addobserver調用單獨釋放。

[emp removeobserver:self forkeypath:@"lastname"];

接着,要接收到通知,你必須重載observevalueforkeypath:<code></code>ofobject:``change:context:方法。object參數辨別你在觀察的對象。keypath參數表示發生改變的屬性。change參數是一個包含所請求的屬性值的映射表。最後,context參數包含注冊成為觀察者時所提供的上下文資料。

2.4.6 單例

單例的最基本的概念是,單例是隻能有一個執行個體對象的類。無論何時從該類請求一個新的對象,你隻會獲得一個指向唯一對象的指針。

單例一般用來表示隻能存在一個執行個體的對象。uiapplication類就是一個很好的例子。每個應用程式隻能有且僅有一個應用程式對象。此外,你可以在任何地方通過[<code>uiapplication sharedapplication</code>]方法來通路應用程式對象。

當然,單例是網絡争論的一個永恒的話題。有些人認為它們是邪惡之源,應該像躲避瘟疫一樣躲避單例。如果你使用了單例,那麼恐怖分子就赢了。或者,稍微理性一點的人則認為,單例隻不過就是一個過度設計的性能不錯的全局變量。

這些抱怨是有一定道理的。當開發者開始接觸到單例模式的時候,他們經常會過度使用它。使用過多的單例會讓你的代碼很難跟蹤。單例也很容易混淆并且很難寫正确(關于如何才算“正确”的了解也很多)。然而,如果能正确使用它們,它們将會難以置信地有用。最重要的是,cocoa touch使用了大量的單例類,是以你至少要懂得基本原理,即使你從來不編寫自己的單例。

下面是一個典型的、相對安全的實作。在類的頭檔案中,聲明一個類方法來通路你的共享執行個體:

上面的代碼使用延遲初始化方式建立共享執行個體(實際上在shared samplesingleton被調用之前我們不會建立執行個體)。在sharedsample<code></code>singleton中,我們使用了dispatch<code>_</code>once程式塊來保護我們的共享執行個體。dispatch<code>_</code>once程式塊以多線程安全的方式確定代碼塊在應用程式的生命周期中隻執行一次。

在arc之前,很多單例的實作會重載許多額外的方法:allocwithzone:,copywithzone:,mutablecopy<code></code>withzone:都是比較常見的需要被重載的方法。鎖住這些方法有助于防止開發者不小心建立單例的副本。然而,當在arc下編譯時,這些方法不能被重載,而且也沒有必要這麼做。蘋果公司目前建議使用一個簡單的單例,依賴慣用法和溝通來防止複制。

我們還會重載父類的指定初始化方法,使得它在被調用時抛出一個異常。隐藏我們的指定初始化方法,并禁用父類的指定初始化方法,進而阻止開發者不小心建立副本(例如,調用[[<code>samplesingleton alloc</code>]<code>init</code>])。

注意copy和mutablecopy預設都是禁用的。由于我們沒有實作copy<code></code>withzone:或者mutablecopywithzone:,這些方法将會自動抛出異常。

這個實作沒有處理從硬碟中加載和儲存單例的微妙問題。如何實作存檔代碼很大程度取決于加載和存儲單例在你的應用程式中意味着什麼。當單例首次建立時你是否從硬碟中加載過?或者加載單例隻是改變單例中所存儲的值嗎?例如,你的應用可能有一個gamestate單例。你隻會有一個gamestate對象,但是狀态值會随着使用者加載和儲存遊戲而改變。

更進階的竅門,一些cocoa touch的單例會讓你在應用程式的info. plist檔案中指定單例的類。這讓你能夠建立單例類的子類,但仍確定在運作時加載正确的版本。如果你需要這些幫助,如下所示修改你的代碼:

以上代碼讀取了info.plist檔案并且查找一個名為samplesingleton的關鍵字。如果找到了一個,它就會将相應的值解釋為類名稱,并且嘗試查找相應的類對象。如果找到了,它就會使用那個類來建立單例對象,否則,它就會使用預設的單例類。

2.4.7 程式塊

唾手可得的程式塊是objective-c 2.0所增加的功能中我最喜歡的。它們隻在ios 4.0和之後的版本才可用。不過,除非你的目标裝置很老,不然程式塊可以極大地簡化很多算法。

例如,uiview類有大量方法使用程式塊來動畫視圖。這些方法比舊的動畫api提供了更加簡潔、更加優雅的解決方案。

程式塊和方法、函數有點相似,它們都是建立可供之後執行的表達式。但是,程式塊可以作為變量存儲,并且可以作為參數傳遞。我們可以像下面這樣建立程式塊變量:

當程式塊捕獲一個棧上存儲的局部變量時,它将這個變量看做一個常量值。你可以讀取這個值但是不能修改它。如果你需要改變一個局部變量,你應該用_``block存儲類型修改器來聲明它。總的來說,它僅僅應用于局部c類型資料和結構體。你也可以改變對象、執行個體變量以及在堆上配置設定的其他任何東西。

然而,通常來說,我們不會建立或者調用程式塊變量,而是将常量程式塊作為參數傳遞給方法和函數。例如,nsarray有一個方法enumerateobjectsusingblock:,它就接受一個程式塊參數。這個方法将會疊代通路數組中的所有對象。對每個對象,它都會調用程式塊,傳入對象、對象的索引以及終止值的引用三個參數。

終止值隻會被用來作為程式塊的輸出。将它設定為yes就中止了enumerateobjectsusingblock:方法。這裡有一個使用這個方法的簡單例子:

注意到我們可以通過傳入選擇器,并且讓我們的枚舉方法為數組中的每個元素調用選擇器,進而複制了enumerateobjectsusingblock: 的功能。我們首先在nsarray中建立一個類别:

這毫無疑問要比我們的程式塊例子笨重一些,但也還不是太壞。然而,那不是重點。如果我們想要改變枚舉器的行為會發生什麼呢?比如我們想要添加一個終止值,當達到這個值時,我們就停止枚舉。

好吧,我們可以将終止參數添加到enumerateobjectsusingtarget<code>:</code>selector:方法中,也可添加到printname:index:方法中。當然,這樣做工作量不會很大,但是改變會影響到整個項目中。更糟糕的是,如果我們這樣操作不止一次,好了,複雜性就會很快地增加。我們會立刻發現自己深陷複雜的枚舉方法叢中,每個方法隻處理輕微不同的情況。

另一個方法,我們可以建立一個執行個體變量來儲存終止名稱,并在printname:index:方法中通路它。那樣的話就避免了改變枚舉方法,但有些草率了。終止名稱不應該是我們類的一部分,我們添加它隻是為了避免額外的參數的權宜之計。那麼如果我們需要幾個不同的行為會發生什麼事呢?我們願意添加多少執行個體變量?

幸運的是,程式塊不會有這些問題。我們能局部地修改enumerate<code></code>objectsusingblock:方法的行為。

請注意,我們完全不需要改變enumerateobjectsusingblock:的實作。我們也不需要任何執行個體變量。更重要的是,所有的一切都維持得很好。

最大的優勢是,解決方案是可擴充的。如果我們想要在别的地方使用不同的行為,沒問題!我們寫一個新的程式塊,捕獲所有我們需要的局部變量,然後調用我們通用的枚舉方法。單個通用的方法就能滿足我們所有的需要。

在arc産生之前,我們都必須小心地使用程式塊。預設情況下,程式塊是在棧上建立的,并且一旦超出作用範圍它們就被銷毀了。為了把程式塊儲存在執行個體變量中以便以後使用,我們必須将程式塊複制到堆上。一旦使用完畢後就要釋放它。幸運的是,arc又一次簡化了這些操作。我們可以建立程式塊、存儲它們、使用它們甚至傳回它們。我們不需要擔心它們是否在堆上或者在棧上。arc為我們處理了所有的複制和管理細節。