首页 > 娱乐前沿 > 热点
基于彻底解耦合的实验性iOS架构
佚名 2016-01-16 19:20:10

这周我决定做一个关于彻底解耦合的应用架构的实验。我想探究的主题是:

“如果所有的应用内通讯都通过一个事件流来完成会怎么样?”

我构造了一个待办事项应用,因为这是我一时激动下所能想到的最原始微型的项目。我会大概地说一下应用结构背后的想法,展示具体实现中的一些代码片段,然后给出几个有关利弊的结论。

整个项目在Github上。作为参考,这篇文章基于0.1标签下的代码。

应用演示

架构总述

为了有一个名字来关联,我把这个架构叫做EventMVVM。它使用少量的MVVM(Model-View-ViewModel)架构。虽然它使用ReactiveCocoa作为事件流的管道,但是我在后面会说到许多工具也可以代替。它是用Swift写的,这有点重要,由于Swift的枚举关联值的特性以及容易定义和使用值类型。

我能够解释架构的最好方法是命名和列举参与者,定义它们,再列出规则。

Event

一个Event是一个消息的构建代码块。定义为枚举,每种情况下都有多达一个相关联的值(注意:这与ReactiveCocoa的Event不同)。你可以把它看作一个强类型NSNotification。每一种情况约定以Request或Response开始。下面是几个例子。

///Event.swift
enumEvent{
//Model
caseRequestReadTodos
caseResponseTodos(Result)
caseRequestWriteTodo(Todo)
//...
//ViewModel
caseRequestTodoViewModels
caseResponseTodoViewModels(Result)
caseRequestDeleteTodoViewModel(TodoViewModel)
//...
}

EventsSignal & EventsObserver

eventsSignal和eventsObserver将是我们共享的事件流。我们将把它们注入进类里,这些类将能够附加观察者块到eventsSignal,并发送新的Event到eventsObserver。

///AppContext.swift
classAppContext{
let(eventsSignal,eventsObserver)=Signal.pipe()
//...
}

我们把这个元组放在一个叫做AppContext的类里。它们使用一个ReactiveCocoa的Signal和一对通过.pipe()创建的观察者来实现。这里有一些实现细节,稍后我们将讨论。

简而言之语法如下:

//在流中创建新的观察者
eventsSignal.observeNext{eventinprint(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:

这些都是我们的请求。它们将所有来自View层。因为这些只是我们广播的事件/消息,不一定有直接的一对一的响应。这对我们同时有积极和消极的后果。

影响之一是我们需要更少类的响应事件。ResponseTodoViewModels和RequestTodoViewModels会有一对一的响应,但RequestToggleCompleteTodoViewModel、RequestDeleteTodoViewModel和RequestUpdateDetailViewModel都会由ResponseTodoViewModel响应。这简化了我们的view的代码,也保证了一个view可以获得更新并传给被一个不同的view改变的ViewModel,我们也不需要额外做什么。

RequestNewTodoDetailViewModel和RequestTodoDetailViewModel(又名新建和编辑)将由ResponseTodoDetailViewModel响应。

有趣的是,RequestUpdateDetailViewModel必须由ResponseUpdateDetailViewModel和ResponseTodoViewModel响应,因为它们的底层待办Model改变了。稍后我们将详细探讨这个场景。

为了满足这些来自View层的请求,ViewModelServer需要有自己的对Model数据的请求。这些都是一对一的请求-响应。

我们在待办Model里通过设置一个flag来实现删除。这种技术明显使它能更容易地协调我们的应用层之间变化。

以下是一个很长的图,有关这四个主要对象如何发送和观察事件。

系统设置

///AppDelegate.swift
classAppDelegate:UIResponder,UIApplicationDelegate{
varappContext:AppContext!
varmodelServer:ModelServer!
varviewModelServer:ViewModelServer!
//...
funcapplication(application:UIApplication,didFinishLaunchingWithOptionslaunchOptions:[NSObject:AnyObject]?)->Bool{
self.appContext=AppContext()
self.modelServer=ModelServer(configuration:Realm.Configuration.defaultConfiguration,appContext:appContext)
self.viewModelServer=ViewModelServer(appContext:appContext)
lettodoListViewModel=TodoListViewModel()
lettodoListViewController=TodoListViewController(viewModel:todoListViewModel,appContext:appContext)
letnavigationController=UINavigationController(rootViewController:todoListViewController)
//...
}
}

正如之前所说,AppContext包含元组eventSignal和eventObserver。我们会将它注入到我们所有的其他高层组件,并允许它们进行交流。

我们必须保留ModelServer和ViewModelServer,因为他们没有view层和互相的直接引用(注解:8)。

记得TodoListViewModel只是一个惰性结构。虽然对于这个简单的应用,我们可以让TodoListViewController创建自己的ViewModel,但是注入是更好的实践途径。你可以很容易地想象把"列表的列表"功能添加到应用。在这种情况下我们(可能?)不需要改变我们的任何接口。

View层:列表

实际上我们的系统边界很清楚。View层将处理所有ViewModel的请求并观察所有ViewModel的响应。

我们这个部分的主题是TodoListViewController。作为参考:

//TodoListViewController.swift
finalclassTodoListViewController:UITableViewController{
letappContext:AppContext
varviewModel:TodoListViewModel
//...
}

我们会发送我们的第一个事件去请求TodoViewModels来填视图出现时的列表。

//TodoListViewController.swift
overridefuncviewWillAppear(animated:Bool){
super.viewWillAppear(animated)
appContext.eventsObserver.sendNext(Event.RequestTodoViewModels)
}

接着我们需要设置一个观察者来响应事件。View层的观察者们总是会放置在viewDidLoad里,同时它的生命周期和UIViewController本身的一样。

overridefuncviewDidLoad(){
//...
appContext.eventsSignal
//...
.observeNext{_in
//...
}
}

剖析一个观察者

现在我们需要深入了解语法。我们所有观察者的结构非常相似:

对于View层,输出的形式通常是副作用(如更新ViewModel和刷新列表)。对于其他Server,输出通常是发送另一个Event。

让我们看看Event.ResponseTodoViewModels。

appContext.eventsSignal
.takeUntilNil{[weakself]inself}//#1
.map{event->Result?in//#2
ifcaselet.ResponseTodoViewModels(result)=event{
returnresult
}
returnnil
}
.ignoreNil()//#2
.promoteErrors(NSError)//#3
.attemptMap{$0}//#3
.observeOn(UIScheduler())//#4
.flatMapError{[unownedself]error->SignalProducerin//#3
self.presentError(error)
return.empty
}
.observeNext{[unownedself]todoViewModelsin//#5
letchange=self.viewModel.incorporateTodoViewModels(todoViewModels)
switchchange{
case.Reload:
self.tableView.reloadData()
case.NoOp:
break
}
}

记住,这个例子实际上是复杂应用的一种,因为有错误处理和多个未展开的阶段。

更多操作

我们将使用UITableViewRowActionde API来发送事件为待办事项标志完成或删除它们。

//TodoListViewController.swift
overridefunctableView(tableView:UITableView,editActionsForRowAtIndexPathindexPath:NSIndexPath)->[UITableViewRowAction]?{
lettodoViewModel=viewModel.viewModelAtIndexPath(indexPath)
lettoggleCompleteAction=UITableViewRowAction(style:UITableViewRowActionStyle.Normal,title:todoViewModel.completeActionTitle){[unownedself](action,path)->Voidin
self.appContext.eventsObserver.sendNext(Event.RequestToggleCompleteTodoViewModel(todoViewModel))
}
//...
return[deleteAction,toggleCompleteAction]
}

这些Event只是修改ViewModel。View层只关心TodoViewModel粒度级别的变化。

我们想要观察ResponseTodoViewModel,这使我们的视图总是显示最准确的待办事项。我们也想有动画效果,因为那样好看。

//TodoListViewController.swift-viewDidLoad()
appContext.eventsSignal
//Event.ResponseTodoViewModel
//...
.observeNext{[unownedself]todoViewModelin
letchange=self.viewModel.incorporateTodoViewModel(todoViewModel)
switchchange{
caselet.Insert(indexPath):
self.tableView.insertRowsAtIndexPaths([indexPath],withRowAnimation:.Top)
caselet.Delete(indexPath):
self.tableView.deleteRowsAtIndexPaths([indexPath],withRowAnimation:.Left)
caselet.Reload(indexPath):
self.tableView.reloadRowsAtIndexPaths([indexPath],withRowAnimation:.Fade)
case.NoOp:
break
}
}

这些都是基本的View层。让我们再看看ViewModelServer,看看我们如何响应这些请求Event和发出新的Event。

ViewModel:列表

ViewModelServer是一个大的配置观察者的init函数。

//ViewModelServer.swift
finalclassViewModelServer{
init(appContext:AppContext){
//...allobserversgohere
}
}

Event.RequestTodoViewModels

ViewModelServer监听ViewModel的请求并发送ViewModel响应Event。

.RequestTodoViewModels相当简单。它只是从model层创建一个相对应的请求(注解12)。

appContext.eventsSignal
//...Event.RequestTodoViewModels
.map{_inEvent.RequestReadTodos}
.observeOn(appContext.scheduler)
.observe(appContext.eventsObserver)

我们把这个事件发回eventsObserver来派遣我们的新Event。注意我们必须派遣这个事件在一个特定的调度器里,否侧会死锁。有关ReactiveCocoa的实现细节超出了本文的范围,所以暂时我们只要注意必须添加任何观察者到新事件的映射。

Event.ResponseTodos

现在我们可以得到一个我们刚刚发出的Model事件的响应。

appContext.eventsSignal
//...Event.ResponseTodos
.map{result->Resultin
returnresult
.map{todosintodos.map{(todo:Todo)->TodoViewModelinTodoViewModel(todo:todo)}}
.mapError{$0}//placeholderforerrormapping
}
.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->Eventin
vartodo=todoViewModel.todo
todo.deleted=true
returnEvent.RequestWriteTodo(todo)
}

用于标记已完成的map块:

.map{todoViewModel->Eventin
vartodo=todoViewModel.todo
todo.completedAt=todo.complete?nil:NSDate()
returnEvent.RequestWriteTodo(todo)
}

简单的转换,然后我们发出一个消息。

这两个事件将在Event.ResponseTodo接收响应。

.map{result->Resultin
returnresult.map{todoinTodoViewModel(todo:todo)}
}
.map{Event.ResponseTodoViewModel($0)}

其他要点

我不会深究其他事件。我只会提一些其他有趣的要点。

TodoDetailViewModel

TodoDetailViewController接受一个TodoDetailViewModel来允许用户去改变其属性。当完成按钮被点击,TodoDetailViewController将用它自己的TodoDetailViewModel发送一个请求到ViewModelServer。ViewModelServer会验证所有的新参数然后回复一个响应。响应事件Event.ResponseUpdateDetailViewModel很有趣,因为它将由三个不同对象的观察。

ResponseUpdateDetailViewModel

我有点想把一般化的CRUD如何进行新建和编辑操作集于一个接口。以前保存过的和未保存的待办事项都可以同样处理。验证被看作是异步的,因此这可以很容易地被当作一个在服务器端的操作。

加载

我没有实现任何加载指示器,只因这是小事。ViewController会观察它自己的Request事件并打开加载指示器作为一个副作用。然后它将关闭加载指示器当作Response事件的副作用。

唯一标识符

有一件事你可能会注意到,在代码库中每一个值类型必须equatable。由于请求和响应不直接配对,有一个惟一标识符是能够过滤和操作响应的关键。实际上在起作用的有两个相等的概念。首先是一般的相等,比如"这两个model有所有参数的值都相同吗?"。第二个是身份的相等,比如"这两个model表示的是相同的底层照片吗?"(即lhs.id == rhs.id)。身份的相等在操作一个已经被更新并且你想替换它的model时,是有用的。

测试

我认为测试明显是在ViewModelServer和ModelServer层。这些Servers注册的观察者在本质上是纯函数,它们收到一个单独的事件并派遣一个单独的事件。一个单元测试示例:

//todostreamTests.swift
//...
functestRequestToggleCompleteTodoViewModel(){
viewModelServer=ViewModelServer(appContext:appContext)
lettodo=Todo()
XCTAssert(todo.complete==false)
lettodoViewModel=TodoViewModel(todo:todo)
letevent=Event.RequestToggleCompleteTodoViewModel(todoViewModel)
letexpectation=expectationWithDescription("")
appContext.eventsSignal.observeNext{(e:todostream.Event)in
ifcaselettodostream.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
//...
functestIntegrationRequestToggleCompleteTodoViewModel(){
viewModelServer=ViewModelServer(appContext:appContext)
modelServer=ModelServer(configuration:Realm.Configuration.defaultConfiguration,appContext:appContext)
lettodo=Todo()
XCTAssert(todo.complete==false)
lettodoViewModel=TodoViewModel(todo:todo)
letevent=Event.RequestToggleCompleteTodoViewModel(todoViewModel)
letexpectation=expectationWithDescription("")
appContext.eventsSignal.observeNext{(e:todostream.Event)in
ifcaselettodostream.Event.ResponseTodoViewModel(result)=e{
letmodel=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有依赖性。

回顾

我们已经看了一个非常基本的应用的一些实现,现在让我们退一步,看看我们一路上发现的利弊。

我完成了构建这个待办事项应用后,我意识到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库。

上一篇  下一篇

I 相关 / Other

源码推荐(01.11B):iOS项目分层,Widget手机任务栏

iOS项目分层(上传者:踏浪帅)主项目中的分层主要包含四个模块,Main(主要)、Expand(扩展)、Resource(照片)、

通过Moya+RxSwift+Argo完成网络请求

作者:@请叫我汪二 授权本站转载。最近在新项目中尝试使用 Moya+RxSwift+Argo 进行网络请求和解析,感觉还阔

Loading动画外篇·圆的不规则变形

一款Loading动画的实现思路系列已经结束了,非常感谢大家的捧场。看过本系列的同学可能还记得,我对原动效做

MVVM没你想象的那么的好

本文由cocoaChina译者 陈溪 翻译作者:Soroush Khanlou原文:MVVM is Not Very Good我写过很多有关于让View

源码推荐(01.12B):代理和block传值区别,仿美团的购物车

代理和block传值区别(上传者:yhj1787354782)此代码主要是区别代理和Block传值,描述如下:A和B两个界面,由A界

I 热点 / Hot