作者:Ben Sandofsky,原文地址
翻譯:BNCoding,如果翻譯有誤感謝指出。
一個結構良好的優秀app應用,必然包含一些功能職責定義良好並且協作高效的簡單物件。當設計一個物件的時候,你一般都會仔細思考物件的屬性和它的行為,但是設計物件之間的溝通協作的設計同樣重要。
蘋果的Cocoa框架只提供了有限的幾種設計模式而且還沒有文件指導我們該怎麼更好的使用這些模式。現在我們來對比一下代理模式和觀察者模式
當我不知道該使用那種模式的時候,我一般都會嘗試使用代理模式。在一對一關係的物件之間這種模式是非常有用的。它的除錯簡單,並且與其它模式相比能夠獲得更多的編譯時檢查。
在大多數的單項通訊中,通知是最理想的一對多關係。例如iOS鍵盤相關的情形。想象一下如果iOS使用應用程式delegate來通知鍵盤事件:
func application(application: UIApplication, willShowKeyboardAtFrame frame:CGRect) {
homeViewController.adjustKeyboardAtFrame(frame)
profileController.adjustKeyboardAtFrame(frame)
messagesController.adjustKeyboardAtFrame(frame)
}
當程式碼中的每一個控制器去處理其子控制器時,這就變成一個非常脆弱,容易出錯的程式碼樣板。如果你決定將它重構成一個易於維護的“全域性排程”的物件時,你就會意識到你其實是在重新建造通知中心。
當濫用通知模式的時候,她就像是使用Cocoa框架程式設計師的goto語句。當一個通知觸發了,一些問題可能在應用程式的任何地方出現,而且還很難預測這些問題出現的順序。然後你就會開始一段令人發瘋的除錯旅程(畫美不看)。
當物件間的通訊變得複雜的時候協議往往適應性會更好。因為通知會使用很多不安全、無謂的執行時連結。
因此應該使用觀察者模式來廣播通知,使用代理來進行通訊交流。
現在,把你的程式想象成現實生活中的商店。大多數的時候你需要的是一對一的交流,就像一個店員告訴顧客某件商品的位於什麼地方。偶爾你才需要對充滿顧客的店內廣播通知,“本店將在半個小時內閉”。
當需要進行一對一交談的時候,你是用了廣播的方式,這就像是在玩telephone game,既不高效又可能導致資訊丟失(注:作者的原意是,如果A想告訴D一件事,不必通過 電視中那種 A告訴B,B告訴C,C再告訴D遊戲的方式,這樣D可能得到的資訊可能不完全正確而且效率也低)。如果有人準備用擴音器告訴你某個訊息的時候,你快速喊停了對方,這樣你也就不需要擔心資訊洩漏了。
示例
例如,我們要編寫一個類似亞馬遜購物一樣的應用程式。這個應用程式肯定包含產品,而且需要展示產品。每一個產品都有圖片、名稱、價格。單個產品的例項可能會在多個地方展示,例如程式主頁,你的購物車等等。
class Product {
var photo:UIImage?
private var photoURL:URL
var price:Float
var name:String
}
我們決定對圖片進行延遲載入(懶載入)。首先我們訪問photo屬性,這會返回nil並且觸發下載動作。幾分鐘之後圖片就下載好能夠展示了。接下來我們將這部分操作進行功能劃分,明確各部分的職責並且將各部分獨立功能連結起來。
第一個問題:應該將網路操作部分的程式碼放在那裡?我們可以把它放在我們的實體中;或者我們可以建立NetworkEntity,然後建立子類物件供app所有物件使用?
相比整合我們更喜歡組合(composition over inheritance)。建立一個其它物件來負責下載資源從而減輕Product物件的職責,這樣對測試來說也更加容易。
當我們正在為app中幾十個獨立物件進行下載資源時,通過這個資源下載的單個示例以漏斗的形式處理這些請求時,操作將變得非常便利,而且我們還能控制請求的速度 或者直接取消請求。那麼怎麼將來兩個部分聯絡起來呢?(點選:it isn’t a singleton)
我們將使用代理。當然,嚴格來說這裡使用的是data source,但是它們的處理是一樣的。
typealias ImageResponse = (UIImage?, NSError?) -> ()
protocol ProductDataSource:class {
func product(product:Product, requestedPhotoWithURL url:NSURL, callback:ImageResponse)
}
class Product {
weak var dataSource:ProductDataSource?
var photo:UIImage?
private var photoURL:URL
var price:Float
var name:String
}
注意我們並沒有在delegate中任何地方說network。它可以使用Http或者從磁碟快取中載入圖片。
作為一個練習,如何像使用通知一樣使用這個API?你可以圍繞像PhotoCacheMissNotfication構建一些東西,但這麼做一定是錯誤的。
通知會在整個應用中暴露狀態。但是在某一個產品請求一個圖片的時候是不是所有的物件都需要知道這個訊息呢?這對於邏輯的蔓延來說很容易,並且將資訊釋出到將它看作API的環境中。這很容易就隱藏了一些會突然的副作用。
思考一個代表產品的tableview cell。上面有一個Add to Cart的按鍵,這個按鍵通知view controller將產品新增到購物車
class ProductCell:UITableViewCell {
var addToCartButton:UIButton!
var product:Product!
}
如果使用target/action的話將會變得很困難,因為你需要知道到底是哪個產品會被新增。使用通知來連結檢視與檢視控制器的是一個反模式,就像在一個AddToCardNotification的userInfo中包含產品資訊。
如果在兩個不同tab頁有同一個view controller類的兩個例項,那麼會發生什麼呢?一個tab可能是”賣的最好的”(Best Sellers),還有一個是”推薦給你的”(Recommended For You)”。兩個view controoler都可以展現同一個產品,那麼這個產品將會新增到購物車兩次。
而且篩選過濾這些資訊也變得更加的困難棘手。Swift中的值型別物件的引數在過濾的時候是無效的,而且如果使用條件判斷語句的話又回讓程式碼變的異常的冗長。因此這裡應該使用代理而不是通知,我們可以定義一個這樣操作,當tableview cell中的button點選時:
protocol ProductCellDelegate:class {
func productCell(cell:ProductCell, didTapAddToCartForProduct product:Product)
}
現在我們再來看看observers模式中的一個比較大的閃光點:那就是在像上面那種一個產品對應多個檢視的這種一對多的關係中。當一個產品的圖片順利的更新完成後,它像展示頁和購物車裡面的產品同時更新。因此圖片載入後,我們發出通知:
let ProductDidUpdateNotification:String
如果我們想去減少那些每個檢視都要跟蹤的變化的話,我們可以通過userInfo的dictionary提供更新的上下文。
總結
在我的簡單例項中,我使用了代理,但你也可以通過回撥(callbacks)構建相似的關係。另外我使用了NSNotificationCenter而不是kvo,至於原因我會在以後進行進一步解釋。