MVVM —您做错了

应用程序体系结构是移动开发中的热门话题,并且有一个原因–每个应用程序都需要某种逻辑形式的结构化代码以保持可靠,可扩展和可维护。 iOS应用程序没有什么不同。 最受欢迎的体系结构之一是Model-View-ViewModel,其中视图控制器和视图属于“视图”部分,而视图模型是负责应用程序业务逻辑的单元。 我本人一直在使用此体系结构,还曾见过其他人的多种实现,但是它们似乎都不令人满意,它们显然错过了一些东西。 这些根组件都不适合执行导航或创建控制器。 在控制器负责这些任务的情况下以MVVM方式执行操作感觉很错误,但是我们必须这样做吗?

在MVVM领域中,用于处理路由的一种可能的解决方案是,视图模型公开一个接口,该接口告诉视图控制器何时何地应该路由到何处。 但是,此解决方案远非理想–它使视图控制器意识到其在应用程序中的位置,从而降低了我们以后重用它的能力。 解决此问题的更好方法是引入一个附加组件,这是经典MVVM所缺少的。 有两个常用的对象–路由器和协调器。 两者都是有效的解决方案,它们使单元测试的某些部分非常简单,但是,有一个关键的区别–路由器从视图控制器的单个实例管理路由,而协调器则负责整个流程。 哪一个更好? 与往常一样-没有适合所有应用程序的灵丹妙药解决方案。 如果您的应用程序有很多独立的屏幕,可以在不同的上下文中显示-您可能应该使用路由器,如果它的屏幕可以分为几个控制器-长流程,则协调器可能是更好的解决方案。

我现在正在处理的应用程序属于第一类,因此我一直在使用路由器。 让我解释一下为什么它们比没有它们的路由有这么大的改进。 请记住,以下大多数属性都是与协调员共享的。

  • 路由器的界面不需要了解UIKit,它所使用的控制器很可能只是暴露了pushpresentdismiss等基本方法的协议,因此,路由器易于测试,并且可以在不考虑平台的情况下使用或设备。
  • 路由器的导航界面是唯一的界面。 如果使用View Controller执行路由,则可能要处理甚至可能不感兴趣的大量方法–同时,路由器的接口仍然很小,非常简单并且可以完全测试。 尽管您不一定需要测试路由器,但是这种简单性可以使视图模型测试更加简洁,因为对路由器的调用通常是复杂的视图模型逻辑的结果。

为了充分利用路由器,我们需要将它们注入到我们的视图模型中。 我们通过为每个路由器使用协议来实现这一点,并且可以解锁更多令人惊奇的特性:

  • 几乎在每个屏幕上都会执行一些与UI相关的常见操作,例如活动指示器的显示和错误/成功消息的显示-这些可以放置在所有路由器的某些根协议中,例如RouterType,因此我们可以避免很多操作不必要的代码重复。
  • 通用路由器功能可以使用默认实现封装在协议中并组成。 例如,我们可能希望我们的路由器能够向SafariController提供一些URL –这不是所有路由器都需要做的,但是我们可能在一些地方使用它。 我们要做的就是创建一个具有默认实现的协议,并且可以与其他协议(例如ImagePickerRoutable或DocumentBrowserRoutable)进一步组合。

路由器的使用还有一个很大的属性-它使提取与导航相关的通用逻辑变得非常容易。 假设您有很多警报,操作表或弹出窗口,需要用户执行某些操作,然后执行一些任务并关闭。 如果随后应执行某些UI动作(例如活动指示器或其他控制器的表示),则视图模型通常会将其通知给视图控制器。 现在,如果需要在多个应用程序中处理相同的动作,我们可以轻松地将逻辑提取到单个可重用的单元中。 但是如何处理那些与导航相关的动作呢? 如果由控制器处理它们,那么所有的人都需要这样做-这可能是大量的代码重复,而且浪费时间。 对于路由器,这个问题根本不存在-我们可以将其与一些常见的处理程序一起重用。

我还要在这里提一些与视图模型相关的实践:

  • 视图模型不应该是数据源,而应该公开单元配置所需的数据-这对于测试复杂的tableViews和collectionViews特别有用。 您还应该将数据源创建为单独的对象-将来可以轻松重用它们,并且立即进行操作不会有任何危害。
  • 用于填充特定视图(例如UserTableViewCell)的数据应包装为单个结构,例如UserCellConfiguration。 这种结构只是实际数据与其转换之间的薄薄一层,用于填充所有标签,imageViews等。 它使实际模型和视图之间的区别更加容易。
  • 使用依赖注入–可以在单元测试中使用模拟对象,使其易于编写。
  • 视图模型不应导入UIKit-这不是至关重要的事情,但是如果您牢记这一点,它将有助于您保持UI与逻辑层之间的分离。

除了介绍路由器之外,我还在应用程序中使用了另一个组件– ControllersBuilder。 ControllersBuilder只是一个简单的结构,能够在整个应用程序中创建所有控制器。

每个构建方法都遵循相同的方案:

  • 初始化路由器并自行注入,因此此类路由器可以请求创建另一个控制器
  • 初始化视图模型,注入所需的依赖关系,初始数据和路由器
  • 初始化视图控制器并注入视图模型
  • 将视图控制器分配给路由器的弱属性

这里要注意的重要事情–最好让构建器方法返回普通的UIViewControllers,而不是返回特定子类的实例。 路由器也是如此–最好保留对UIViewController的引用。 它不是强制性的,但是,到目前为止,这是我一直在做的方式,它可以帮助我尽可能地保持那些对象的哑巴。

引入路由器和DI的最大好处之一就是它如何改进单元测试。 我想向您展示其中之一在我的项目中的样子:

路由器只是注入的一种协议,用于查看模型,而且由于依赖注入,我们还可以模拟服务响应,从而使我们的测试非常容易编写和读取-上面的测试也不例外,它只是MVVM-R现实的一部分。

这是另一个例子。 这次呈现快照测试:

这简直太棒了–多亏了DI和ControllersBuilder,我们可以用几行代码对应用程序中的每个屏幕进行快照测试。

在过去的五个月中,我一直在可用于生产的应用程序中大量使用此体系结构,我对它如何提高工作质量感到非常满意:

  • 职责分散在各个组成部分中,
  • 依赖关系是可注入的,
  • 向逻辑单元添加新的依存关系就像通过另一种协议扩展类型别名一样容易,
  • 控制器是可重用的
  • 可以轻松完成对现有代码的修改,并且对整个逻辑进行了很好的测试。

总体而言,与之共事是一种乐趣。

结果呢? 该应用程序已成功发布到AppStore,有近8000名用户,我们在第一周就设法实现了接近99%的无崩溃率,仅记录了一次崩溃。 作为开发人员,我必须说–这种体系结构及其启用的可测试性使我在发布新功能或重构时充满信心。

免责声明–我自己并没有提出描述的体系结构,它是由我的同事Szymon Mrozek向我介绍的,非常感谢他。 您可以在这里查看他的文章。

以下是一些其他有用的链接:

  1. 协议组成–很棒的文章,向我介绍了这个概念。 这正是我的应用处理依赖关系的方式。
  2. 协调器–路由器替代概念的介绍,我之前已经提到过。
  3. App Architecture –关于体系结构的精彩讨论,重点强调了一些要点,其中一些要点可以使您的应用程序立即受益。
  4. Sourcery –用于生成样板代码的工具,我正在使用它来创建所有模拟。

如果您喜欢这篇文章,请在下面的评论中告诉我您的想法。


最初在 appunite.com上 发布