单一职责原则在 iOS 中的应用

2,193

原文链接

本文翻译自 “Cleaner Architecture on iOS”, 作者 Tomas Hakel

这篇文章不打算讲一些新的或有创新性的东西,而是来讨论一个广为人知的东西:单一责任原则(SRP)。更具体的说,我想讨论的是如何在 Clean Architecture 中来正确地使用它。我们应该经常提醒自己,在做决策时考虑一下 SRP,以此帮助我们设计出更好的软件。

SRP 是什么?

先来看看 WIKI 上的定义:

The single responsibility principle is a computer programming principle that states that every module or class should have responsibility over a single part of the functionality provided by the software, and that responsibility should be entirely encapsulated by the class. All its services should be narrowly aligned with that responsibility.

单一责任原则是一种计算机编程原则,规定每个模块或类只对软件提供的功能的一部分负责,并且该职责应完全由类封装。其所有服务应与该职责严格一致。

一个类应该只有一个改变的因素。遵循这个原则,可以获得诸多好处:使修改变得更容易、减少耦合、提高可测试性、加快开发速度等等。SRP 也是 Clean Architecture 的基本思想(适用于 MVC、MVVM、响应式等等)。

为什么不使用 MVC?

最直接的答案是:Massive View Controller。尽管这只是个玩笑话,不过实际情况确实如此。这也引出了另一个问题:为什么 MVC 是 iOS 上的默认架构,即便它会导致诸如 Massive View Controller 这样的问题?答案是,它不是首要问题。如果 Controller 很庞大,这并不是架构的错,而是程序员没有正确地使用它。你可以使用 MVC 编写出结构非常清晰的程序,同时可以通过以下方法轻松处理 Massive View Controller 问题:

  • 一个场景使用多个控制器;
  • 将任务委托给 worker/service 类;

一句话:使用 SRP。那么,如果 MVC 不是问题,那为什么要使用其它框架呢?原因是正如在 iOS 上的应用一样,MVC 存在一些问题,这使它会界定职责时比较模糊,留给程序员做的决定太多。比如:控制器的责任是什么?如果你不小心一点,那它所承担的就很多。那我们应该把一些需要的工作放在哪里呢?在 model 层?我们又如何去构建?MVC 并没有明确的告诉我们如何去做。这就意味着我们有很大机率引入错误。

如果我们不想考虑这些问题,就可以使用 Clean Architecture。Clean Architecture 明确划分了类的职责:Presenter 负责桥接 UI 层和业务逻辑层;Interactor 处理我们的用例;路由帮我们跳转到新页面。权责清晰,这样我们的代码就会更干净整洁。

那么,使用 Clean Architecture,可以解决 MVC 的问题么?在我们的代码中应用了 SRP 了么?不一定。这个架构非常有用,但并没有解决所有问题。我们仍然需要思考、做出选择,并努力让代码变得更加清晰。

MIP 问题

在 Apple 平台上,Clean Architecture 已经非常流行。我们有很多种选择,如 VIPERClean Swift。在这里让我们来看一些 Clean Swift 的实际例子。

我见过一些有大型的复杂 Interactor 的工程,这些工程并不遵循 SRP。我称之为 Massive Interactor Problem(MIP)。MIP 可能并不像 Massive View Controller 问题那么糟糕,因为 Interactor 并不关心 UI,但它依然试图去做太多的事情。问题在于,尽管我们认为自己正在使用 Clean Architecture,但并没有恰当地去划分职责。为了从根本上避免 MVC / MIP 问题,我们需要更多地使用 SRP。

Interactor 包含了应用程序的业务逻辑,但每个 View Controller 只有一个 Interactor。在小场景中,这没有问题,但正如在一个场景中只使用一个 View Controller 一样,在大的场景中,Interactor 同样面临着失控的局面。我的观点是,在复杂的场景中,Interactor不应该真正去处理任何事情:没有算法,没有数据库操作或数据解析,没有任何需要嵌套的 if 或循环语句。这些任务应该由 Worker 类来处理。Interactor 的职责是将任务交给各种 Worker ,并把结果传递给 Presenter。Clean Swift 的示例代码中,Worker 只包含一小段代码,由于比较简短,所以没有体现出它的重要性。示例这么做是没错的,毕竟 Worker 的职责是处理特定的业务逻辑。不过,我觉得需要特别强调一下 Worker 的重要性。

在 Clean Architecture 中,一个 Interactor 应该表示一个单独的用例。这意味着一个类应该只有一个关注点。在 Clean Swift 中,一个场景只有一个 Interactor,而一个场景通常会包含多个用例,所以 Interactor 会有很多职责。在问题比较简单时,我们可以选择忽略这一点,但随着 Interactor 的增长,它会变得越来越不受控制。另一种选择是将这些职责代理给 Worker 类。在这种情况下,Interactor 将创建多个 Worker。当 UI 发起一个请求时,Interactor 将简单地将请求发送给 Worker 处理,并将结果传回给 Presenter。这听起来很简单,但却大大改善了 MIP 问题。由于一个 Worker 只处理用例,它看上去更像一个 Interactor,不是么?

VIPER 是什么?

我听很多人说 VIPER 很复杂:需要花很长的时间去配置,有太多很小的类。但 VIPER 很好的实践了 SRP。如果你更关注 SRP,并将 SRP 应用于你的 MVC 工程,最后也可能会产出类似的框架(类似的事情就发生在我身上)。

Clean Swift 和 VIPER 非常相似,毕竟都是基于 Clean Architecture。主要的区别在于 VIP cycle,以及每个场景的 Interactor 的数量。但如果我们在 Clean Swift 中严格遵循 SRP,将 Clean Swift Interactor 的职责划分清楚,并为每个用例创建一个 Worker 类,基本上就是 VIPER 了(如果我们忽略一些细节,就可以将 Clean Swift 的 Worker 与 VIPER 的 Interactor 对应起来,而 Clean Swift 的 Presenter-Interactor 可以对应 VIPER 的 Presenter)。事实上,由于有 VIP cycle,Clean Swift 显得更复杂。VIPER 并不像人们所说的那么复杂。所以不要担心它所提供的东西。即使在下一个项目中你不用使用 VIPER,但去理解它,以及它的运作方式,也是大有益处的。而使用它,可以让自己和他人的工作更加轻松。

More on SRP

这里有一些建议:

  • 不要混淆抽象层。在上层业务逻辑中执行底层操作是违反 SRP 原则的。毕竟我们在处理组织或器官时,不希望去考虑细胞的复杂性,反之亦然;
  • 明确类的职责,并将其写在类上方的注释中。描述应简明扼要,同时包含所有职责。不过要留心 “manager” 这样的词。manager 用来做什么?如果不注意的话,这样的类很容易包含多个职责。当修改类时,请确保它的职责不变,否则就重构它;
  • SRP 可应用于多个级别,从通用架构到单个方法。如果一个方法做了多件事,就将这些操作分解几个新方法中,让每个方法执行特定的任务,然后在原方法中调用。记住,一个方法应该只在一个抽象层中;
  • 大段的代码意味着破坏了 SRP。如果你发现在 IDE 中滚动了很长时间,那么代码可能出问题了。SRP 是一个比行数更好的度量标准,所以不要找一个魔数来规定一个文件中代码的行数,而是列出职责。如果不止一个,就进行拆分。较短的类和方法也更容易维护,所以尽量保持合理的代码长度;
  • 务实,而不是完全遵循规范和指南,了解为什么这样设计以及想要达到的目标。然后做出明知的选择。

总之,不要写出 Massive Controllers。明确职责,确保在一个地方没有太多的职责(理想情况下,2个就是太多了)。在为类添加代码时,需要考虑类的职责,而不是简单的添加。从长远来看,遵循 SRP 最有可能节省时间 - 所以不要找借口。

资源

  1. en.wikipedia.org/wiki/Single…
  2. 8thlight.com/blog/uncle-…
  3. clean-swift.com/clean-swift…
  4. objc.io/issues/13-a…

关注我们

欢迎关注我们的公众号:iOS-Tips,也欢迎加入我们的群组讨论问题。可以公众号留言 iosflutterwebpwa小程序 等关键词获取入群方式。