本文由CocoaChina譯者@ALEX吳浩文翻譯,歡迎指正。
作者:Chris
原文:An Experimental iOS Architecture Based on Radical Decoupling
這周我決定做一個關於徹底解耦合的應用架構的實驗。我想探究的主題是:
“如果所有的應用內通訊都通過一個事件流來完成會怎麼樣?”
我構造了一個待辦事項應用,因為這是我一時激動下所能想到的最原始微型的項目。我會大概地說一下應用結構背後的想法,展示具體實現中的一些代碼片段,然後給出幾個有關利弊的結論。
整個項目在Github上。作為參考,這篇文章基於0.1標簽下的代碼。
應用演示
架構總述
為了有一個名字來關聯,我把這個架構叫做EventMVVM。它使用少量的MVVM(Model-View-ViewModel)架構。雖然它使用ReactiveCocoa作為事件流的管道,但是我在後面會說到許多工具也可以代替。它是用Swift寫的,這有點重要,由於Swift的枚舉關聯值的特性以及容易定義和使用值類型。
我能夠解釋架構的最好方法是命名和列舉參與者,定義它們,再列出規則。
Event
EventsSignal & EventsObserver
Server
Model
ViewModel
View
Event
一個Event是一個消息的構建代碼塊。定義為枚舉,每種情況下都有多達一個相關聯的值(注意:這與ReactiveCocoa的Event不同)。你可以把它看作一個強類型NSNotification。每一種情況約定以Request或Response開始。下面是幾個例子。
/// Event.swift enum Event { // Model case RequestReadTodos case ResponseTodos(Result) case RequestWriteTodo(Todo) // ... // ViewModel case RequestTodoViewModels case ResponseTodoViewModels(Result) case RequestDeleteTodoViewModel(TodoViewModel) // ... }
Model和ViewModel"類型"的事件都包括在Event枚舉中(注解:1)。
RequestReadTodos沒有參數,因為這個應用不需要預先篩選或排序(注解:2)。
我們使用Result來封裝返回值或異常(注:3)。
所有枚舉項的關聯值都是值類型,這對於確保系統的健全是很重要的。同一個Event可能被任何一個的線程上的許多對象接收到。
EventsSignal & EventsObserver
eventsSignal和eventsObserver將是我們共享的事件流。我們將把它們注入進類裡,這些類將能夠附加觀察者塊到eventsSignal,並發送新的Event到eventsObserver。
/// AppContext.swift class AppContext { let (eventsSignal, eventsObserver) = Signal.pipe() // ... }
我們把這個元組放在一個叫做AppContext的類裡。它們使用一個ReactiveCocoa的Signal和一對通過.pipe()創建的觀察者來實現。這裡有一些實現細節,稍後我們將討論。
簡而言之語法如下:
// 在流中創建新的觀察者 eventsSignal.observeNext { event in print(event) } // 在流中發送一個新的事件 eventsObserver.sendNext(Event.RequestTodoViewModels)
Server
Server是一個長久存活的類,它包含觀察者並能發送消息。在我們的示例應用中,有兩個Server--ViewModelServerhe和ModelServer。這些都是由AppDelegate創建並持有的。從名字你可能會認為ViewModelServer設置了我們應用的ViewModel相關的職責的觀察者。例如,它負責為ViewModels接收請求並滿足它們,不是改變事件裡的ViewModel,就是發送一個新的事件請求它需要的數據(注解:4,注解:5)。
Server代表我們應用裡的"智能"對象。它們是協調器。它們創建和操縱我們的ViewModel、Model值類型,並與其他server通過創建Event和附加在它們之上的值進行交流。
Model
一個Model是一個包含基本數據的值類型。在標准MVVM裡,它不應該包含任何一個針對底層數據庫的東西。
在示例應用中,我用擴展來把Todo model對象序列化成TodoObject用於我們的Realm數據庫。
模型層只知道自己。它不知道ViewModel和View。
ViewModel
一個ViewModel是一個值類型,它包含在View層裡並且是一個可以直接使用的屬性。例如,UILabel顯示的文本就該是一個String。ViewModel在init函數裡接收和存儲一個Model對象,並將之轉變為View層可使用的。一個ViewModel可使其他ViewModels能夠被子視圖等使用。
按這種解釋(注解:6),ViewModels是完全惰性的,並且不能異步操作和向事件流發送消息。這確保它們可以安全地在線程間傳遞。
ViewModel不知道View層。它們可以操作其他ViewModel和Model。
View
我們的View層是UIKit,包括UIViewControllers和UIViews及其子類。雖然我的初衷是探索讓View層也通過事件流發送自己的事件,但是在這個簡單的實現裡卻是不必要的,並且可能是最使人分心的(注解:7)。
View層只允許與View和ViewModel層進行交互。這意味著它對Model一無所知。
實現
現在我們對所有的組件系統已經有了一個基本的了解,讓我們深入進代碼,看看它是如何工作的。
The Spec(軟件規格說明書)
我們的待辦列表的特點是什麼?這類似於我們的Event。(對我來說,這是最激動人心的部分。)Event.swift:
RequestTodoViewModels:我們希望能夠看到所有待辦事項按預設順序排序,並過濾掉已刪除的條目。
RequestToggleCompleteTodoViewModel:我們需要能夠在列表視圖把待辦事項標記為完成。
RequestDeleteTodoViewModel:我們也需要能夠將在列表視圖刪除它們。
RequestNewTodoDetailViewModel:我們需要能夠創建新的待辦事項。
RequestTodoDetailViewModel:我們需要能夠漂亮地查看/編輯一個待辦事項。
RequestUpdateDetailViewModel:我們需要能夠提交我們的更改。
這些都是我們的請求。它們將所有來自View層。因為這些只是我們廣播的事件/消息,不一定有直接的一對一的響應。這對我們同時有積極和消極的後果。
影響之一是我們需要更少類的響應事件。ResponseTodoViewModels和RequestTodoViewModels會有一對一的響應,但RequestToggleCompleteTodoViewModel、RequestDeleteTodoViewModel和RequestUpdateDetailViewModel都會由ResponseTodoViewModel響應。這簡化了我們的view的代碼,也保證了一個view可以獲得更新並傳給被一個不同的view改變的ViewModel,我們也不需要額外做什麼。
RequestNewTodoDetailViewModel和RequestTodoDetailViewModel(又名新建和編輯)將由ResponseTodoDetailViewModel響應。
有趣的是,RequestUpdateDetailViewModel必須由ResponseUpdateDetailViewModel和ResponseTodoViewModel響應,因為它們的底層待辦Model改變了。稍後我們將詳細探討這個場景。
為了滿足這些來自View層的請求,ViewModelServer需要有自己的對Model數據的請求。這些都是一對一的請求-響應。
RequestReadTodos -> ResponseTodos
RequestWriteTodo -> ResponseTodo
我們在待辦Model裡通過設置一個flag來實現刪除。這種技術明顯使它能更容易地協調我們的應用層之間變化。
以下是一個很長的圖,有關這四個主要對象如何發送和觀察事件。
系統設置
/// AppDelegate.swift class AppDelegate: UIResponder, UIApplicationDelegate { var appContext: AppContext! var modelServer: ModelServer! var viewModelServer: ViewModelServer! // ... func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool { self.appContext = AppContext() self.modelServer = ModelServer(configuration: Realm.Configuration.defaultConfiguration, appContext: appContext) self.viewModelServer = ViewModelServer(appContext: appContext) let todoListViewModel = TodoListViewModel() let todoListViewController = TodoListViewController(viewModel: todoListViewModel, appContext: appContext) let navigationController = UINavigationController(rootViewController: todoListViewController) // ... } }
正如之前所說,AppContext包含元組eventSignal和eventObserver。我們會將它注入到我們所有的其他高層組件,並允許它們進行交流。
我們必須保留ModelServer和ViewModelServer,因為他們沒有view層和互相的直接引用(注解:8)。
記得TodoListViewModel只是一個惰性結構。雖然對於這個簡單的應用,我們可以讓TodoListViewController創建自己的ViewModel,但是注入是更好的實踐途徑。你可以很容易地想象把"列表的列表"功能添加到應用。在這種情況下我們(可能?)不需要改變我們的任何接口。
View層:列表
實際上我們的系統邊界很清楚。View層將處理所有ViewModel的請求並觀察所有ViewModel的響應。
我們這個部分的主題是TodoListViewController。作為參考:
// TodoListViewController.swift final class TodoListViewController: UITableViewController { let appContext: AppContext var viewModel: TodoListViewModel // ... }
我們會發送我們的第一個事件去請求TodoViewModels來填視圖出現時的列表。
// TodoListViewController.swift override func viewWillAppear(animated: Bool) { super.viewWillAppear(animated) appContext.eventsObserver.sendNext(Event.RequestTodoViewModels) }
接著我們需要設置一個觀察者來響應事件。View層的觀察者們總是會放置在viewDidLoad裡,同時它的生命周期和UIViewController本身的一樣。
override func viewDidLoad() { // ... appContext.eventsSignal // ... .observeNext { _ in // ... } }
剖析一個觀察者
現在我們需要深入了解語法。我們所有觀察者的結構非常相似:
生命周期
過濾
拆箱
映射
錯誤處理
輸出
對於View層,輸出的形式通常是副作用(如更新ViewModel和刷新列表)。對於其他Server,輸出通常是發送另一個Event。
讓我們看看Event.ResponseTodoViewModels。
appContext.eventsSignal .takeUntilNil { [weak self] in self } // #1 .map { event -> Result? in // #2 if case let .ResponseTodoViewModels(result) = event { return result } return nil } .ignoreNil() // #2 .promoteErrors(NSError) // #3 .attemptMap { $0 } // #3 .observeOn(UIScheduler()) // #4 .flatMapError { [unowned self] error -> SignalProducer in // #3 self.presentError(error) return .empty } .observeNext { [unowned self] todoViewModels in // #5 let change = self.viewModel.incorporateTodoViewModels(todoViewModels) switch change { case .Reload: self.tableView.reloadData() case .NoOp: break } }
#1:這是一個ReactiveCocoa實現細節,它(相當於注解9)把觀察者生命周期限制在self的生命周期裡。換句話說,當TodoListViewController消失時,停止處理這個觀察者。
#2:這裡是我們在必要時從事件中過濾和拆包的地方。記住,我們在觀察整個應用發送的Event的消防帶。我們只想要Event.ResponseTodoViewModels,並且如果得到,我們希望它的值被傳遞。對於其他所有到達的事件,它們會被映射到nil然後被ignoreNil()運算符丟棄。
#3:這是我們的錯誤處理。promoteErrors是一個ReactiveCocoa的實現細節,它將一個無法報錯的信號轉化成一個能發送錯誤到指定類型的信號。然後attemptMap從Result對象中拆包,並允許我們使用ReactiveCocoa內建的錯誤處理。flatMapError就是我們錯誤的副作用,在這種情況下,錯誤以警報形式呈現。相反,如果我們用observeError,我們的觀察者將在第一個錯誤事件後被處理掉,這不是我們想要的(注解10)。
#4:Event可以被eventsSignal交付到任何線程。因此,對於任何線程的關鍵工作我們需要指定目標調度器。在這種情況下,我們的關鍵工作是UI相關,因此我們使用UIScheduler。注意,只有在observeOn之後的操作能夠在UIScheduler上執行(注解11)。
#5:最後,我們有一個來自正確的事件的非錯值。我們將使用這個完全取代TodoListViewModel並且有條件地刷新列表,如果列表有任何真正的改變。
記住,這個例子實際上是復雜應用的一種,因為有錯誤處理和多個未展開的階段。
更多操作
我們將使用UITableViewRowActionde API來發送事件為待辦事項標志完成或刪除它們。
// TodoListViewController.swift override func tableView(tableView: UITableView, editActionsForRowAtIndexPath indexPath: NSIndexPath) -> [UITableViewRowAction]? { let todoViewModel = viewModel.viewModelAtIndexPath(indexPath) let toggleCompleteAction = UITableViewRowAction(style: UITableViewRowActionStyle.Normal, title: todoViewModel.completeActionTitle) { [unowned self] (action, path) -> Void in self.appContext.eventsObserver.sendNext(Event.RequestToggleCompleteTodoViewModel(todoViewModel)) } // ... return [deleteAction, toggleCompleteAction] }
這些Event只是修改ViewModel。View層只關心TodoViewModel粒度級別的變化。
我們想要觀察ResponseTodoViewModel,這使我們的視圖總是顯示最准確的待辦事項。我們也想有動畫效果,因為那樣好看。
// TodoListViewController.swift - viewDidLoad() appContext.eventsSignal // Event.ResponseTodoViewModel // ... .observeNext { [unowned self] todoViewModel in let change = self.viewModel.incorporateTodoViewModel(todoViewModel) switch change { case let .Insert(indexPath): self.tableView.insertRowsAtIndexPaths([indexPath], withRowAnimation: .Top) case let .Delete(indexPath): self.tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .Left) case let .Reload(indexPath): self.tableView.reloadRowsAtIndexPaths([indexPath], withRowAnimation: .Fade) case .NoOp: break } }
這些都是基本的View層。讓我們再看看ViewModelServer,看看我們如何響應這些請求Event和發出新的Event。
ViewModel:列表
ViewModelServer是一個大的配置觀察者的init函數。
// ViewModelServer.swift final class ViewModelServer { init(appContext: AppContext) { // ... all observers go here } }
Event.RequestTodoViewModels
ViewModelServer監聽ViewModel的請求並發送ViewModel響應Event。
.RequestTodoViewModels相當簡單。它只是從model層創建一個相對應的請求(注解12)。
appContext.eventsSignal // ... Event.RequestTodoViewModels .map { _ in Event.RequestReadTodos } .observeOn(appContext.scheduler) .observe(appContext.eventsObserver)
我們把這個事件發回eventsObserver來派遣我們的新Event。注意我們必須派遣這個事件在一個特定的調度器裡,否側會死鎖。有關ReactiveCocoa的實現細節超出了本文的范圍,所以暫時我們只要注意必須添加任何觀察者到新事件的映射。
Event.ResponseTodos
現在我們可以得到一個我們剛剛發出的Model事件的響應。
appContext.eventsSignal // ... Event.ResponseTodos .map { result -> Result in return result .map { todos in todos.map { (todo: Todo) -> TodoViewModel in TodoViewModel(todo: todo) } } .mapError { $0 } // placeholder for error mapping } .map { Event.ResponseTodoViewModels($0) } .observeOn(appContext.scheduler) .observe(appContext.eventsObserver)
我們把Result<[todo], nserror="">映射到Result<[todoviewmodel], nserror="">,並返回result作為一個新的Event。有一個占位符,在我們可以將Model層的錯誤映射到一個更適合展示給用戶的地方(注解13)。
其他ViewModel事件
在view層,我們看到兩個事件,RequestToggleCompleteTodoViewModel和RequestDeleteTodoViewModel,可能被發送來動態地改變個別ViewModels。
用於刪除的map塊:
.map { todoViewModel -> Event in var todo = todoViewModel.todo todo.deleted = true return Event.RequestWriteTodo(todo) }
用於標記已完成的map塊:
.map { todoViewModel -> Event in var todo = todoViewModel.todo todo.completedAt = todo.complete ? nil : NSDate() return Event.RequestWriteTodo(todo) }
簡單的轉換,然後我們發出一個消息。
這兩個事件將在Event.ResponseTodo接收響應。
.map { result -> Result in return result.map { todo in TodoViewModel(todo: todo) } } .map { Event.ResponseTodoViewModel($0) }
其他要點
我不會深究其他事件。我只會提一些其他有趣的要點。
TodoDetailViewModel
TodoDetailViewController接受一個TodoDetailViewModel來允許用戶去改變其屬性。當完成按鈕被點擊,TodoDetailViewController將用它自己的TodoDetailViewModel發送一個請求到ViewModelServer。ViewModelServer會驗證所有的新參數然後回復一個響應。響應事件Event.ResponseUpdateDetailViewModel很有趣,因為它將由三個不同對象的觀察。
TodoDetailViewController將觀察它的錯誤。如果有錯誤的驗證,它將在當前上下文前展現錯誤。
TodoListViewController將觀察非錯值,作為一個用戶結束編輯ViewModel的信號去解釋它,然後它應該彈回TodoDetailViewController。
ViewModelServer將觀察其本身將發送的消息,因為現在它必須立即創建一個更新待辦事項Model並發送一個寫待辦事項的Event。它的響應會通過正常的Event流傳回並由TodoListViewController透明地更新。
ResponseUpdateDetailViewModel
我有點想把一般化的CRUD如何進行新建和編輯操作集於一個接口。以前保存過的和未保存的待辦事項都可以同樣處理。驗證被看作是異步的,因此這可以很容易地被當作一個在服務器端的操作。
加載
我沒有實現任何加載指示器,只因這是小事。ViewController會觀察它自己的Request事件並打開加載指示器作為一個副作用。然後它將關閉加載指示器當作Response事件的副作用。
唯一標識符
有一件事你可能會注意到,在代碼庫中每一個值類型必須equatable。由於請求和響應不直接配對,有一個惟一標識符是能夠過濾和操作響應的關鍵。實際上在起作用的有兩個相等的概念。首先是一般的相等,比如"這兩個model有所有參數的值都相同嗎?"。第二個是身份的相等,比如"這兩個model表示的是相同的底層資源嗎?"(即lhs.id == rhs.id)。身份的相等在操作一個已經被更新並且你想替換它的model時,是有用的。
測試
我認為測試明顯是在ViewModelServer和ModelServer層。這些Servers注冊的觀察者在本質上是純函數,它們收到一個單獨的事件並派遣一個單獨的事件。一個單元測試示例:
// todostreamTests.swift // ... func testRequestToggleCompleteTodoViewModel() { viewModelServer = ViewModelServer(appContext: appContext) let todo = Todo() XCTAssert(todo.complete == false) let todoViewModel = TodoViewModel(todo: todo) let event = Event.RequestToggleCompleteTodoViewModel(todoViewModel) let expectation = expectationWithDescription("") appContext.eventsSignal.observeNext { (e: todostream.Event) in if case let todostream.Event.RequestWriteTodo(model) = e { XCTAssert(model.complete == true) expectation.fulfill() } } appContext.eventsObserver.sendNext(event) waitForExpectationsWithTimeout(1.0, handler: nil) }
上面的部分測試了一個ViewModelServer裡的觀察者,並在ViewModelServer和ModelServer之間的邊界等待獲得結果Event。
集成測試也不是不可能。以下是一個相同事件的集成測試版本,它不再等待在View和ViewModelServer層之間的邊界:
// todostreamTests.swift // ... func testIntegrationRequestToggleCompleteTodoViewModel() { viewModelServer = ViewModelServer(appContext: appContext) modelServer = ModelServer(configuration: Realm.Configuration.defaultConfiguration, appContext: appContext) let todo = Todo() XCTAssert(todo.complete == false) let todoViewModel = TodoViewModel(todo: todo) let event = Event.RequestToggleCompleteTodoViewModel(todoViewModel) let expectation = expectationWithDescription("") appContext.eventsSignal.observeNext { (e: todostream.Event) in if case let todostream.Event.ResponseTodoViewModel(result) = e { let model = result.value!.todo XCTAssert(model.id == todo.id) XCTAssert(model.complete == true) expectation.fulfill() } } appContext.eventsObserver.sendNext(event) waitForExpectationsWithTimeout(1.0, handler: nil) }
在這種情況下,後台有兩個其他事件同時發送,但是我們只等待最後一個。
這兩個server都處在表層,只對EventSignal有依賴性。
回顧
我們已經看了一個非常基本的應用的一些實現,現在讓我們退一步,看看我們一路上發現的利弊。
利 一些在其他架構很難的事情變容易了!:D
弊 一些在其他架構很簡單的事情變難了!:(
利 實際上這種代碼風格很有趣。
弊 可能有目前未知的性能影響,考慮到存在很多觀察者,每個又接收大量的必須過濾的事件。
利 線程似乎很安全。
弊 仍然有很多沒有解決的問題。如何處理圖像加載?身份驗證?特定順序的多步操作?列表重排序?更復雜的視圖改變類型?其他異步API封裝?問題是無止境的。一個半生不熟的待辦事項應用幾乎沒有擴大系統復雜性的范圍。
利 所有代碼(除了UIKit)風格都很相似且非常實用。
弊 所有的事件是全局的(對於系統來說),因此在系統規模和復雜性上增長後,更多的意想不到的後果可能發生。
弊 在觀察者聲明裡有相當數量的同樣格式的陳詞濫調。
利 更容易理清對象的所有者和生命周期。
弊 使用Result來異常處理並不適合。我直覺有另一個能做得更好的辦法,我需要研究(注解14)。
利 測試可以說是一個相當無痛的過程。
利 使得"重放"用戶的整個會議過程成為可能,通過管道傳輸,從eventsSignal的序列化保存的輸出到eventsObserver的一個新的會話。
利 分析會變得很容易,設置作為一個單獨的Server-type對象,當它們被放到流中可以監聽Event並轉換然後在必要時POST到服務器。
庫
我完成了構建這個待辦事項應用後,我意識到ReactiveCocoa不一定是最好的實現EventMVVM的工具。它的很多特性我並沒用到,我有一些怪癖,它旨在被用而我卻不使用它(注解15)。
我決定去試試我可不可以寫我自己的簡單的為EventMVVM量身定做的庫來實現EventMVVM。我花了一天的事件來與這個類型系統搏斗,只因為我有一個最先的念頭--我要繼續試著測試。它只有大約100行代碼。不幸的是,它不能自動化所有我想要的東西,觀察的過程仍有缺點。我會找時間寫一些關於這個庫的事。
你可以在Github看到我的進展。
總結
探索EventMVVM架構很有趣。我可能會繼續探索它,作為兼職。我絕對不會建議用它來實現任何重要的東西。
如果你有任何關於EventMVVM的想法,請通過Twitter讓我知道。我確定這種風格已經有個名稱(也許是觀察者模式?)。
只要添加這個觀察者到AppDelegate,就能獲取到系統中傳遞的每個Event的日志,該有多酷?
appContext.eventsSignal.observeNext { print($0) }
1.未來EventMVVM的擴展可以是ModelEvent或ViewModel事件,並且每一個有輸入流。這樣,一個View對象只會看到ViewModel流,而ViewModelServers(稍後我會介紹它)會看到ViewModel和Model流。
2.一個更復雜的應用,將需要一個ReadTodosRequest結構來封裝一個分類描述符和謂詞。或者更好的是,一個更徹底的TodoListViewModel包含所有這些信息。
3.事實證明,在響應本身嵌入一個可選的錯誤參數會更好。否則,就無法知道這個錯誤與哪個請求相關。我們暫時不考慮這個問題。
4.你當然可以把ViewModelServer和ModelServer合並到一個Server(或把一切都放在AppDelegate),但MVVM是幫助我們分離我們關心的事。
5.我最大的一個未解決的問題是如果Server對象相互大量創建該怎麼辦。任何像樣的應用,一個ViewModelServer的一個流裡有成百上千的觀察者是很笨拙的。它們也可能使用了太多的資源。如果我們把每個ViewModel類型分離出ViewModelServers,那麼主ViewModelServer怎麼知道如何管理它們的生命周期?
6.我大多數其他使用MVVM的項目,有些ViewModel是類並且挑起大部分關於異步工作和應用內數據流組織的重擔,有些則是惰性的值類型。這背後的原因是通過分離邏輯來讓ViewControllers有點"遲鈍"。
7.這些類型的事件的例子有ViewControllerDidBecomeActive(UIViewController)和ButtonWasTapped(UIButton)。正如你所看到的,這將打破我們只有通過流發送值類型的假設,並且更需要深思熟慮。當我在工作中把它與其他框架一起使用時發現,你可以跳過很多的障礙來避免UIKit期望你去做的方式,雖然通常你想出來另一種方式會更糟。
8.在"經典"MVVM裡,View將擁有ViewModel的,ViewModel將擁有Model/Controller。
9.准確來說,觀察者將被觸發轉換到完成狀態,當任何事件被發送並且自身不再是活動的。就我們的目的而言,這不該是一件大事。雖然還有其他的辦法來解決這個問題,但是它們需要更多語法上的要求。
10.回想起來,讓Result一路穿過observeNext並在同一代碼塊內處理成功和錯誤的情況,可能更清晰。
11.Scheduler是ReactiveCocoa原生的。它很巧妙。
12.如果你不熟悉MVVM,您可能想知道為什麼View層不直接發出Event.RequestReadTodos而是通過ViewModelServer傳送Event.RequestTodoViewModels。一個受歡迎的間接層是讓我們的View層不知道所有與Model層相關的事務。它引入了對自己和項目中的其它人的可預測性,所有類型的對象和值遵守同一套規則--哪些它們可以做,哪些對象它們可以交互。這顯然是一般化的,而且感覺它存在項目的早期,但在大型項目我很少發現它會被毫無根據的優化。
13.不包括Model層的枚舉類型錯誤是因為懶。我們設立的轉換管道已經能夠很容易讓我們對於正確的上下文正確地表示。
14.提示:這是添加一個error參數到所有model和ViewModel。
15.它可以用NSNotificationCenter實現(並非我有試過)。也可以用其他的Reactive Swift庫。
本文僅用於學習和交流目的,轉載請注明文章譯者、作者、出處以及本文鏈接。
感謝博文視點對本期翻譯活動的支持