干净的快速架构可替代VIPER

引言


目前,有许多关于VIPER的文章-干净的体系结构,其各种变体一度在iOS项目中变得很流行。 如果您不熟悉Viper,可以在此处此处此处阅读

我想谈谈VIPER替代方案-Clean Swift。 乍一看,Clean Swift看起来像VIPER,但是,通过研究模块之间的交互原理,这些差异变得显而易见。 在VIPER中,交互的基础是Presenter,它将用户请求传输到Interactor进行处理,并将从其接收的数据格式化回显示在View Controller上:

图片

在Clean Swift中,像VIPER中一样,主要模块是View Controller,Interactor,Presenter。

图片

它们之间的相互作用是周期性发生的。 数据传输基于协议(同样类似于VIPER),当将来系统组件发生更改时,可以将其更改为另一种协议。 交互过程通常如下所示:用户单击按钮,View Controller创建带有描述的对象,并将其发送给Interactor。 然后,Interactor根据业务逻辑实施特定方案,创建结果对象并将其传递给Presenter。 Presenter形成一个对象,该对象的数据具有可显示给用户的格式,并将其发送给View Controller。 让我们更详细地了解每个Clean Swift模块。

视图(视图控制器)


与VIPER中一样,View Controller执行所有VIew配置,无论是颜色,UILabel还是布局字体设置。 因此,此体系结构中的每个UIViewController都实现了用于显示数据或响应用户操作的Input协议。

牵连器


交互器包含所有业务逻辑。 它接受来自控制器的用户操作,并带有输入协议中定义的参数(例如,输入字段的更改文本,按下按钮)。 弄清楚逻辑之后,如有必要,Interactor必须将准备数据传输到Presenter,然后再在ViewController中显示。 但是,与VIPER不同,Interactor仅接受来自View的请求作为输入,在VIPER中,这些请求通过Presenter。

主讲人


演示者处理数据以显示给用户。 这种情况下的结果是ViewController的Input协议,在这里您可以例如更改文本格式,将颜色值从枚举转换为rgb等。

工人


为了避免不必要地使Interactor复杂化并且不重复业务逻辑的细节,可以使用附加的Worker元素。 在简单模块中,并非总是需要它,但是在负载足够的模块中,它允许您从Interactor中删除某些任务。 例如,与数据库交互的逻辑可以在worker中进行,特别是如果可以在不同的模块中使用相同的数据库查询时。

路由器


路由器负责将数据传输到其他模块以及它们之间的转换。 他有一个到控制器的链接,因为不幸的是,在iOS中,从历史上看,控制器一直负责转换。 使用segue可以通过从Prepare for segue调用Router方法来简化转换的初始化,因为Router知道如何传输数据,并且无需来自Interactor / Presenter的任何额外循环代码就可以进行传输。 使用在Interactor中实现的每个模块的数据仓库协议来传输数据。 这些协议还限制了从路由器访问内部模块数据的能力。

型号


模型是对用于在模块之间传输数据的数据结构的描述。 业务逻辑功能的每种实现都有自己的模型描述。

  • 请求-将请求从控制器发送到交互器。
  • 响应-交互者将数据发送给演示者的响应。
  • ViewModel-用于以准备在控制器中显示的形式进行数据传输。

实施实例


让我们通过一个简单的例子仔细研究一下这种架构。 它们将由ContactsBook应用程序以简化的方式提供,但对于理解体系结构形式的本质而言已经足够了。 该应用程序包括联系人列表,以及添加和编辑联系人。

输入协议的示例:

protocol ContactListDisplayLogic: class { func displayContacts(viewModel: ContactList.ShowContacts.ViewModel) } 

每个控制器都包含对实现输入Interactor协议的对象的引用

 var interactor: ContactListBusinessLogic? 

以及Router对象,该对象应实现数据传输和模块切换的逻辑:

 var router: (NSObjectProtocol & ContactListRoutingLogic & ContactListDataPassing)? 

您可以在单独的私有方法中实现模块配置:

 private func setup() { let viewController = self let interactor = ContactListInteractor() let presenter = ContactListPresenter() let router = ContactListRouter() viewController.interactor = interactor viewController.router = router interactor.presenter = presenter presenter.viewController = viewController router.viewController = viewController router.dataStore = interactor } 

或创建Configurator单例以从控制器中删除此代码(对于那些认为控制器不应该参与配置的人员),并且不要诱使自己访问控制器中模块的各个部分。 Bob叔叔的视图和经典VIPER中没有配置程序类。 将配置器用于添加联系人模块如下所示:

 override func awakeFromNib() { super.awakeFromNib() AddContactConfigurator.sharedInstance.configure(self) } 

配置器代码包含唯一与控制器中的设置方法完全相同的配置方法:

 final class AddContactConfigurator { static let sharedInstance = AddContactConfigurator() private init() {} func configure(_ control: AddContactViewController) { let viewController = control let interactor = AddContactInteractor() let presenter = AddContactPresenter() let router = AddContactRouter() viewController.interactor = interactor viewController.router = router interactor.presenter = presenter presenter.viewController = viewController router.viewController = viewController router.dataStore = interactor } } 

控制器实现中的另一个非常重要的一点是标准的pregue for segue方法中的代码:

 override func prepare(for segue: UIStoryboardSegue, sender: Any?) { if let scene = segue.identifier { let selector = NSSelectorFromString("routeTo\(scene)WithSegue:") if let router = router, router.responds(to: selector) { router.perform(selector, with: segue) } } } 

细心的读者很可能注意到,还需要Router来实现NSObjectProtocol。 这样做是为了使我们可以在使用segues时使用此协议的标准方法进行路由。 为了支持这种简单的重定向,segue标识符的命名应与Router方法名称的结尾匹配。 例如,要查看联系人,有一个segue,它与选择带有联系人的单元格有关。 它的标识符是“ ViewContact”,这是路由器中的相应方法:

 func routeToViewContact(segue: UIStoryboardSegue?) 

向Interactor显示数据的请求看起来也很简单:

 private func fetchContacts() { let request = ContactList.ShowContacts.Request() interactor?.showContacts(request: request) } 

让我们继续进行交互。 Interactor实现了ContactListDataStore协议,该协议负责存储/访问数据。 在我们的案例中,这只是一个接触数组,仅受getter方法限制,以向路由器展示不允许从其他模块更改它。 实现我们列表的业务逻辑的协议如下:

 func showContacts(request: ContactList.ShowContacts.Request) { let contacts = worker.getContacts() self.contacts = contacts let response = ContactList.ShowContacts.Response(contacts: contacts) presenter?.presentContacts(response: response) } 

它从ContactListWorker接收联系数据。 在这种情况下,工作人员负责如何下载数据。 他可以求助于第三方服务,这些服务决定例如从缓存中获取数据或从网络中下载数据。 收到数据后,Interactor会向演示者发送响应以准备显示,因为该Interactor包含指向演示者的链接:

 var presenter: ContactListPresentationLogic? 

Presenter仅实现一种协议-ContactListPresentationLogic,在我们的示例中,它只是简单地强制更改联系人的姓和名的大小写,从数据模型中形成DisplayedContact演示模型,并将其传递给Controller进行显示:

 func presentContacts(response: ContactList.ShowContacts.Response) { let mapped = response.contacts.map { ContactList .ShowContacts .ViewModel .DisplayedContact(firstName: $0.firstName.uppercaseFirst, lastName: $0.lastName.uppercaseFirst) } let viewModel = ContactList.ShowContacts.ViewModel(displayedContacts: mapped) viewController?.displayContacts(viewModel: viewModel) } 

之后,循环结束,控制器显示数据,实现ContactListDisplayLogic协议方法:

 func displayContacts(viewModel: ContactList.ShowContacts.ViewModel) { displayedContacts = viewModel.displayedContacts tableView.reloadData() } 

这是用于显示联系人的模型的样子:

 enum ShowContacts { struct Request { } struct Response { var contacts: [Contact] } struct ViewModel { struct DisplayedContact { let firstName: String let lastName: String var fullName: String { return firstName + " " + lastName } } var displayedContacts: [DisplayedContact] } } 

在这种情况下,请求不包含数据,因为这只是一般的联系人列表,但是,例如,如果列表屏幕包含过滤器,则该请求中可以包含过滤器类型。 Intrecator响应模型包含所需的联系人列表,ViewModel还包含准备显示的数据数组-DisplayedContact。

为什么要清理Swift


考虑这种架构的优缺点。 首先,Clean Swift具有代码模板,可简化模块的创建。 这些模板可以为许多体系结构编写,但是当它们开箱即用时-至少可以节省几个小时的时间。

其次,像VIPER一样,此体系结构也经过了良好的测试,项目中提供了测试示例。 由于与之发生交互的模块很容易用存根替换,因此使用协议确定每个模块的功能可让您轻松实现此目的。 如果我们同时创建业务逻辑和相应的测试(Interactor,Interactor测试),则这很符合TDD的原理。 由于逻辑的每种情况的输出和输入都是由协议定义的,因此只需编写一个确定其行为的测试,然后直接实现方法逻辑就足够了。

第三,Clean Swift(与VIPER不同)实现了数据处理和决策的单向流程。 始终只执行一个循环-视图-交互器-演示者-视图,这也简化了重构,因为通常需要更改较少的实体。 因此,使用Clean Swift方法可以更方便地重构经常更改或补充逻辑的项目。 使用Clean Swift,您可以通过两种方式分离实体:

  1. 通过声明输入和输出协议来隔离组件
  2. 通过使用结构和将数据封装在单独的请求/响应/ UI模型中来隔离功能。 每个功能都有其自己的逻辑,并且可以在一个过程的框架内进行控制,而不会在一个模块中与其他功能相交。

如果没有长远的眼光,则不应在原型项目中将Clean Swift用于小型项目。 例如,使用此体系结构为开发者会议的时间表实施应用程序太昂贵了。 相反,长期项目,具有大量业务逻辑的项目非常适合此体系结构的框架。 当该项目在两个平台(Mac OS和iOS)上实现,或者计划在将来移植时,使用Clean Swift非常方便。

Source: https://habr.com/ru/post/zh-CN415725/


All Articles