VIPER-S:编写您自己的体系结构以了解其重要性(第1部分)

最初发布于ThinkAndBuild:http://www.thinkandbuild.it/viper-s-writing-your-own-architecture-to-understand-its-importance-part-1/

在为我的应用程序使用VIPER几个月后,我开始研究自己的体系结构:我想为自己的需求创建更好的东西。 然后,我开始与同事Marco交流思想。 他站在Android的一面,但我们需要讨论以找到共同点,并取得一致的结果。

我们“有点”失败了,最终得到了与VIPER真正相似的东西,但是! 我现在正在应用程序中使用的是这个VIPER的修订版,因此我不会认为这是一次失败的尝试。 更像是VIPER的自定义版本。

通过这一途径,我学到了很多关于体系结构的知识,因此我决定与一系列文章分享经验。 我想重点关注两件事:

•为获得完整架构而做出的决策,突出了基本原理和疑点(其中有些仍然存在)

•我最终使用的体系结构显示了代码和一些实际示例。

从现在开始,我们将此结构称为VIPER-S 。 这里的S代表语义,因为我试图获得一种更清晰的命名方式,赋予角色和通信更多的意义,并添加了一些规则来提高代码的可读性和编写性。

让我们的建筑师

让我们开始一个问题的旅程。 为什么需要架构? 这个问题有很多不同的答案,但最相关的是:

•清楚了解我们的代码(并简化其维护)

•轻松分配职责(并简化团队工作)

•提高可测试性(并简化您的生活)

牢记这些答案,并怀着深远的目标感,我们可以开始规划架构。

我是“ divide-et-impera”的忠实拥护者:对我来说,这是一种生活方式。 这就是为什么我将首先确定所有领域角色 ,将在这些领域工作的参与者以及这些参与者之间如何交流的原因。 这些元素将定义我们的体系结构的支柱,因此,清楚地了解它们是什么真的很重要。

域是一个大集合,其中包含职责范围的所有逻辑。 角色只是这个角色的一小部分,它更加具体,可以确定确切的需求。 角色是实现所有功能以满足角色的代码元素。

让我们列出并描述我为构建VIPER-S而确定的领域和角色。

体系结构域:用户界面

通过用户界面域,我们向用户显示信息并与他们进行交互。 让我们看看该域的角色。

角色:显示UI信息

这是一个真正的“愚蠢”角色。 到达该域的数据已准备就绪,可以使用,无需进一步处理。 它只需要向下发送到最终的UI元素,并具有以下功能:

  func display(date:String){label.text = date} 

如您所见,在上一步中,date属性可能已从Date转换为String。 我们仅在此处显示现成的信息 。 标签显示一个字符串,因此我们希望收到一个字符串。

角色:处理UI事件

这是另一个不太主动的角色,实际上,我们在这里仅拦截用户交互或应用程序生命周期事件。 通常在UI target-action中调用负责该角色的参与者

  @IBAction func save(){eventsHandler.onSave()} 

建筑领域:数据

数据域中,我们从源那里获取信息,然后将其转换为稍后提供的信息,或者将用户操作处理为可以存储在某处或以某种方式使用的信息。 这是数据域的角色。

角色:管理数据

让我们将此角色想象为一组负责处理特定工作的工人。 他们只知道如何完成工作,并且会在操作完成或失败时通知其他人。

这是此角色的函数的简单(不安全)示例:

  func fetchItems(){ 
  networkManager.get(位于:itemsPath){ 
(已完成,项目)在
 如果(已完成){ 
presenter.present(items:项目)
}

其他{
presenter.presentItemFetchError()
}
  } 
}

工作人员正在使用网络管理器来获取项目。 它确切地知道如何使用管理器,但是它不适用于来自网络的任何值,它只是将值传递给对象,而对象又知道如何呈现它。

角色:当前数据

让我们提醒自己不要将呈现与显示混淆:当呈现信息时,我们将其转换为稍后将通过UI显示的内容。 经常从“管理数据”角色中调用实现此角色的对象。 返回到用户界面的上一个示例,我们不在此处设置标签的文本值。 相反,我们将Date转换为可读的String。

 功能存在(日期:日期){ 
 让dateString = date.localizedString(“ YYYY-mm-dd”) 
ui.display(date:dateString)
}

建筑领域:导航

该域具有一个角色:处理应用程序的导航 。 如何显示“下一个视图”背后的逻辑完全在此角色内处理,其初始化和解雇也是如此。 然后,我们需要使用UIKit与Storyboard一起使用,并调用所有必需的默认iOS导航功能。

  func navigationToDetail('for'item:Item){ 

让itemDetail = ItemDetailNavigator.makeModule(with:item)
push(nextViewController:itemDetail)
}

在此示例中,导航器正在构建我们将要展示的模块 (稍后将在本术语上作更多介绍–现在将其视为ViewController),并将其推送到当前导航堆栈。

域之间的通信

现在让我们介绍该架构的第一个参与者“导演”。 稍后我们将详细查看其代码。 现在,让我们将其作为在我们刚刚看到的域之间架起桥梁的方式进行讨论。

主管负责驱动信息流从UI事件到数据处理,再到从数据处理回到UI。 它还负责定义何时必须进行导航。 每个操作都从导向器开始,并且每个操作的结果(在某个时候)都将通过它。

让我们检查到目前为止讨论的体系结构概述,以更好地了解通信是如何发生的:

所有这些箭头……但是请相信我,流程比看起来容易。 这是一个真实的示例:当用户点击“保存”按钮时,应用程序必须保存一些信息并显示带有成功消息的对话框(如果出现错误,则显示错误)。

该流程将从“处理事件”的上一张图片的左侧开始。 用户的点击被拦截并传递给导演。 主管将信息发送到负责“管理数据”角色的对象。 当该对象完成操作后,就可以将结果呈现给导演,导演现在将信息发送回UI域,而后者又知道如何显示它。 现在让我们说,在保存操作结束时,我们宁愿转到另一个页面,也不愿显示弹出窗口。 简单。 导演可以将其驱动到导航,而无需将流移到UI域。

我们的代码

现在终于到了将架构逻辑转换为代码的时候了!

在开始此过程之前,我们需要为我们要实现的示例确定所需的模块。 什么是模块? 该体系结构考虑了一个模块,我们可以简单地将其称为应用程序的“页面”或“视图”。 这意味着对于一个可以列出,显示和添加项目的应用程序,您有3个模块。 例如,使用MVC架构,每个模块将是一个视图控制器。

让我们介绍将在这些教程中实现的代码示例。 我们正在编写一个应用程序来处理可以启用或禁用的通用“项目”。 一个项目具有名称,创建日期和状态(启用或禁用)。 我们将实现3个模块来处理项目:“项目列表”,“添加项目”和“项目详细信息”。 除了上面的欢迎页面,我们总共有4个模块,分为2组:项目和常规。

组织您的项目

我是一个杂乱无章的人,因此在进行大型项目时,需要遵循严格的架构。 对于VIPER-S,我决定使用非常清晰的文件夹结构 。 毕竟,这是定义体系结构的一部分。

每个模块组都有一个根文件夹。 在这种情况下,“常规”和“项目”(我宁愿为模块组创建真实文件夹,而不仅仅是Xcode项目文件夹)。 每个模块都有其自己的文件夹。 对于项目,我们具有“添加”,“列表”和“详细信息”,对于“常规”仅具有“欢迎”。

这是项目的当前文件夹结构:

每个文件和类都遵循一个简单的命名约定 :使用包含该文件和类的文件夹结构作为前缀,然后以其专门名称命名。 例如,“项目”的“列表”模块的主管称为“ ItemsListDirector.swift”,类名称为“ ItemsListDirector”。 与自动完成功能一起使用时,这将非常有用。 当您开始编写“ List…”时,您将获得该组的所有类。 然后“…添加”仅获取ListAdd模块的类。 这是一个非常方便的约定!

稍后我们将讨论其他命名约定。 这只是一条简单的规则,可为名称定义和项目组织创建共享逻辑。 如果您像我一样,真的不擅长在很长的项目过程中保持您的命名风格不变,那将是救生员。

合同和协议的定义

让我们开始写一个合约,该合约通过协议描述每个模块的体系结构。 合同是体系结构的一部分,您可以在其中精确定义模块的功能。 这是该模块的一种文档。

我们将从“项目列表”模块开始,将先前描述的角色转换为协议。 对于此模块,我们知道它通过表格显示“项目”列表,并且具有“全部删除”按钮以刷新所有可用项目。

“显示UI”角色必须显示项目,错误和成功消息。 描述此角色的一个好的协议是:

 协议ItemList_DisplayUI { 
 功能显示(项目:[ItemUI]) 
功能显示(错误:字符串)
func displaySuccess()
}

itemUI是由简单类型(如String,UIImage或Bool)定义的基础对象。 我们稍后再讨论。
使用UI模型更新UI元素的所有功能均以“ display”字词作为前缀。 严格遵守命名约定很重要,因为我不想怀疑“我应该将此功能称为“显示”,“显示”,“更新” WAT吗?!”。 所有协议都有一组预定义的动词/关键字要使用。

注意:这是我正在使用的另一个小命名约定。 考虑到我们最终将为单个模块获得大量文件和类,因此我发现将协议与类区分开来很有用。 这就是为什么我在模块名称和角色名称之间加下划线,以获取协议名称(ItemList_DisplayUI)。 相信我,以后,当您编写自己的代码并且想要快速自动完成类或协议名称时,您会喜欢上这个小技巧。

“处理UI事件”角色具有3个功能:必须说UI就绪时(即,调用viewDidLoad时),当用户点击“全部删除”按钮时,它必须触发一个事件,而当用户点击“全部删除”按钮时,必须触发另一个事件。从表中选择项目。

 协议ItemsList_HandleUIEvents { 
  func onUIReady() 
func onDeleteAll()
func onItemSelected(index:Int)
}

通常,此角色的功能以前缀“ on”开头,后跟已处理事件的名称。

让我们继续到Data域。 第一个角色是“管理数据”,它具有两个功能:获取项目并删除所有项目。

 协议ItemsList_ManageData { 
  func fetchItems() 
func deleteAllItems()
}

第二个角色是“当前数据”。 有了这个角色,我们希望在可用时显示项目,也可以显示一般的成功或错误消息。

 协议ItemsList_PresentData { 
  func present(项目:[项目]) 
func presentSuccess()
func presentError()
}

就我个人而言,我喜欢这种表示法,并且发现它极易读。 动词“ present”是所有协议功能的前缀。

导航域的唯一角色是“导航”。 从ItemsList模块中,我们知道我们可以选择一个项目并在专用视图中查看其详细信息。 我们还可以返回到“欢迎”视图,或更笼统地说,我们可以返回至上一个视图。

 协议ItemsList_Navigate { 
  func gotoDetail('for'item:Item) 
func goBack()
}

导航协议的功能以“ go / goto”为前缀。

 协议ItemsList_Navigate { 
  func gotoDetail(`for` item:Item) 
func goBack()
}

这是ItemsListProtocol.swift文件的完整代码。 如您所见,如果您知道每个角色的功能,就可以轻松了解此模块的作用:

 协议ItemList_DisplayUI { 
功能显示(项目:[ItemUIModel])
func displayError()
func displaySuccess()
}

协议ItemsList_HandleUIEvents {
func onUIReady()
func onDeleteAll()
func onItemSelected(index:Int)
}

协议ItemsList_ManageData {
func fetchItems()
func deleteAllItems()
}

协议ItemsList_PresentData {
func present(项目:[项目])
func presentSuccess()
func presentError()
}

协议ItemsList_Navigate {
func gotoDetail(`for` item:Item)
func goBack()
}

到此结束系列的第一部分。 在接下来的部分中,我们将更深入地研究体系结构的代码,编写所有涉及的参与者。 我们将完成ItemList模块,并讨论如何处理某些特定模式,例如将信息传递给另一个模块(即,当您选择一个Item并导航到详细信息页面时)以及从另一个模块获取信息(即,当您在ItemsAdd模块中添加新的Item,您需要通知ItemsList模块以刷新列表)。

感谢您阅读本文并继续关注本系列的下一期。 再见!

下载源关注@bitwaker