尝试在 iOS 应用上安装 "clean architecture"

Trying to fit a "clean architecture" on an iOS app

最近我一直在重新思考我的 android architecture project, trying to adapt it to a more “clean architecture”, specifically, the kind of design suggested by “Uncle Bob”

它涉及多个抽象层、很好的职责隔离和通过依赖注入实现的非常强的依赖倒置;这最终导致了一个非常解耦的便携式系统。通过单元测试和集成测试进行测试的完美候选者。

在我的 android 实现中,我最终拥有三个不同的模块或层:

-领域:实体、交互者、演示者(纯java模块)

-数据:(充当向域提供数据的存储库)(android 库模块)

-演示文稿:ui 相关内容、片段、活动、视图等(android 应用程序模块)

所以,我想弄清楚什么是 iOS 生态系统的最佳方法。 我尝试创建一个包含多个目标的项目来实现相同的解决方案:

-domain: command line target(这看起来很奇怪,但我认为是可用的最纯粹的swift目标)

-数据: cocoa触摸框架

-演示: cocoa 触控框架

通过这种方法,我可以像使用 android 模块一样使用这些目标。但我发现的第一个警告是我需要手动将每个新文件添加到依赖目标。

但是我对多目标项目的了解非常有限。我的意思是我从未创建过具有多个目标的 iOS 应用程序。所以我不知道解决方案是否会使用框架 (cocoa touch/cocoa) 作为目标而不是域层的命令行模块。

如有任何想法,我们将不胜感激。

谢谢!

Uncle Bob 的简洁架构绝对适用于 iOS、Swift 和 Obj-C。架构与语言无关。 Bob 大叔自己的代码大多在 Java 中,但在他的演讲中他很少提到 Java。他的所有幻灯片甚至没有显示任何代码。它是一种适用于任何项目的架构。

为什么我这么肯定?因为我研究了2年MVC、MVVM、ReactiveCocoa、Clean Architecture。到目前为止,我最喜欢 Clean Architecture。我通过将 7 个 Apple 示例项目转换为使用 Clean Architecture 来测试它。我专门使用这种方法已经一年多了。每次效果都更好。

一些好处是:

  • 更快更轻松地查找和修复错误。
  • 将视图控制器中的业务逻辑提取到交互器中。
  • 将视图控制器中的表示逻辑提取到演示器中。
  • 通过快速且可维护的单元测试充满信心地改变现有行为。
  • 编写具有单一职责的较短方法。
  • 解耦 class 具有明确边界的依赖关系。

我们还添加了一个路由器组件,因此我们可以使用多个故事板。不再有冲突。

编写单元测试也大大简化了,因为我只需要在边界测试方法。我不需要测试私有方法。最重要的是,我什至不需要任何模拟框架,因为编写自己的模拟和存根变得微不足道。

我已经写下了我在 Clean Swift 学习 iOS 架构的过去 2 年的经验 我还整理了一些 Xcode 模板来生成所有 Clean Architecture 组件以节省很多时间。

更新 - 在下面的评论中回答@Víctor Albertos 关于依赖注入的问题。

这是一个非常好的问题,需要一个很详细的答案。

永远牢记 VIP 周期。在这种情况下,doSomethingOnLoad() 方法不是 boundary 方法。相反,它是一种仅在 CreateOrderViewController 中调用的 内部 方法。在单元测试中,我们测试单元的预期行为。我们提供输入,观察输出,然后将输出与我们的期望进行比较。

是的,我可以将 doSomethingOnLoad() 设为私有方法。但我选择不这样做。 Swift 的目标之一是让开发人员可以轻松编写代码。 inputoutput 协议中已经列出了所有边界方法。确实没有必要在 class 中乱加无关的私有修饰符。

现在,我们确实需要以某种方式测试 "The CreateOrderViewController should do something on load with this request data" 的这种行为,对吗?如果我们不能调用 doSomethingOnLoad() 因为它是私有方法,我们如何测试它?你打电话给viewDidLoad()viewDidLoad() 方法是一种边界方法。哪个边界?用户和视图控制器之间的界限!用户对设备做了一些事情以使其加载另一个屏幕。那么我们如何调用 viewDidLoad() 呢?你这样做:

let bundle = NSBundle(forClass: self.dynamicType)
let storyboard = UIStoryboard(name: "Main", bundle: bundle)
let createOrderViewController = storyboard.instantiateViewControllerWithIdentifier("CreateOrderViewController") as! CreateOrderViewController
let view = createOrderViewController.view

只需调用 createOrderViewController.view 属性 就会调用 viewDidLoad()。我很久以前就从某人那里学到了这个技巧。但是Natasha The Robot最近也提到了。

当我们决定测试什么时,只测试边界方法是非常重要的。如果我们测试 class 的每个方法,测试就会变得非常脆弱。我们对代码所做的每一次更改都会破坏很多很多测试。很多人因此放弃了。

或者,这样想。当您询问如何模拟 CreateOrderRequest 时,首先询问 doSomethingOnLoad() 是否是您应该为其编写测试的边界方法。如果不是,那是什么?在这种情况下,边界方法实际上是 viewDidLoad()。输入是 "when this view loads." 输出是 "call this method with this request object."

这是使用 Clean Swift 的另一个好处。您所有的边界方法都列在文件顶部的显式命名协议 CreateOrderViewControllerInputCreateOrderViewControllerOutput 下。您无需寻找其他地方!

想想如果你要测试 doSomethingOnLoad() 会发生什么。您模拟请求对象,然后断言它等于您期望的请求对象。您正在嘲笑某些东西并进行比较。就像 assert(1, 1) 而不是 var a=1; assert(a, 1)。重点是什么? 太多 测试。太脆弱.

现在,有一段时间你可以模拟 CreateOrderRequest。在您验证视图控制器组件可以生成正确的 CreateOrderRequest 之后。当您测试 CreateOrderInteractordoSomething() 边界方法时,您然后使用接口依赖注入模拟 CreateOrderRequest

简而言之,单元测试并不是要测试 class 的每个单元。它是关于将 class 作为一个单元进行测试。

这是一种思维方式的转变。

希望对您有所帮助!

我在 Wordpress 中有 3 个系列的不同主题的草稿帖子:

  1. 深入了解每个 Clean Swift 组件
  2. 如何将复杂的业务逻辑分解为工作人员和服务对象。
  3. 在 Clean Swift iOS 架构中编写测试

你想先听哪一个?我应该提高测试系列吗?