swift 的依赖注入,两个 UIViewController 的依赖图没有共同的父对象

Dependency Injection with swift with dependency graph of two UIViewControllers without common parent

当我们有两个在层次结构中非常深的 UIViewController 时,我们如何在不使用框架的情况下应用依赖注入,并且它们都需要保存状态的相同依赖项,而这两个 UIViewController 没有共同的父级。

示例:

VC1 -> VC2 -> VC3 -> VC4

VC5 -> VC6 -> VC7 -> VC8

假设 VC4 和 VC8 都需要 UserService 来容纳当前用户。

请注意,我们要避免单例。

有没有一种优雅的方法来处理这种 DI 情况?

经过一番研究,我发现有人提到 Abstract FactoryContext interfacesBuilderstrategy pattern

但是我找不到关于如何在 iOS

上应用它的示例

好的,我试试看。

你说的是"no singleton",所以我在下面排除了,但也请看这个答案的底部。

Josh Homann 的评论已经很好地指出了一种解决方案,但就我个人而言,协调器模式存在问题。

正如 Josh 正确所说的那样,视图控制器不应该相互了解(太多)[1],但是例如协调员或任何依赖项已通过 around/accessed?有几种模式可以说明如何操作,但大多数都有一个基本上与您的要求背道而驰的问题:它们或多或少地使协调器成为单身人士(本身或作为另一个单身人士的 属性,如 AppDelegate).协调器也常常是单身人士(但并非总是如此,也不一定如此)。

我倾向于依赖简单的初始化属性或(最常见)惰性属性和面向协议的编程。让我们构建一个例子:UserService 应该是定义你的服务需要的所有功能的协议,MyUserService 它的实现结构。让我们假设 UserService 是一个设计结构,它基本上充当一些用户相关数据的 getter/setter 系统:访问令牌(例如保存在钥匙串中)、一些偏好(头像图像的 URL)之类的。在初始化时 MyUserService 还准备数据(例如从远程加载)。这将在几个独立的 screens/view 控制器中使用,而不是单例。

现在每个有兴趣访问此数据的视图控制器都有一个简单的 属性:

lazy var userService: UserService = MyUserService()

我保留它 public 因为这允许我在单元测试中轻松 mock/stub 它(如果我需要这样做,我可以创建一个虚拟 TestUserService mocks/stubs 行为)。实例化也可以是一个闭包,如果 init 需要参数,我可以在测试期间轻松切换。显然,属性甚至不一定需要 lazy,具体取决于对象的实际作用。如果提前实例化对象没有害处(记住单元测试,也包括传出连接),只需跳过 lazy.

技巧显然是设计UserServiceand/orMyUserService创建它的多个实例时出现问题。然而,我发现这在 90% 的情况下并不是真正的问题,只要实例应该依赖的实际数据保存在其他地方,在一个单一的事实点,比如钥匙串,一个核心数据堆栈,用户默认值或远程后端。

我知道这是一种逃避的回答,因为在某种程度上我只是在描述一种方法,这种方法(至少是其中的一部分)有许多通用模式。然而,我发现这是在 Swift 中处理依赖注入的最通用和最简单的形式。协调器模式可以与其正交使用,但我发现它在日常使用中较少"Apple-like"。它确实解决了一个问题,但大多数情况下,你得到它的那个你没有按照预期正确使用故事板(特别是:只是将它们用作 "VC repos",从那里实例化它们并在代码中转换自己)。

[1] 除了一些基本的 and/or 小事外,您可以传入完成处理程序或 prepareForSegue。这是有争议的,取决于您对协调员或其他模式的遵守程度。就我个人而言,我有时会在这里走捷径,只要它不会膨胀并变得凌乱即可。一些弹出式设计更简单。


作为结束语,短语 "Note that we want to avoid Singleton" 以及您在问题下对此的评论给我的印象是您只是听从了该建议,而没有适当考虑其基本原理。我知道 "Singleton" 通常被认为是一种反模式,但这种判断经常被误导。单例可以是一个有效的架构概念(您可以从它在框架和库中广泛使用的事实中看出这一点)。它的坏处在于,它常常诱使开发人员在设计中走捷径并将其滥用为一种 "object repository",这样他们就不需要考虑何时何地实例化对象。这会导致混乱和模式的坏名声。

A UserService,取决于它在您的应用程序中实际执行的操作 可能 是一个很好的单例候选者。我个人的经验法则是:"If it manages state of something that's singular and unique, like a specific user that can only ever be in one state at a given time",我可能会选择单身。

特别是如果你不能按照我上面概述的方式设计它,即如果你需要内存中的单一状态数据,单例基本上是一个简单且正确的 方法来实现它。 (即使使用(惰性)属性是有益的,您的视图控制器甚至不需要知道它是否是单例,您仍然可以 stub/mock 它单独(即不仅仅是全局实例)。)

我发现 coordinator/router 设计模式最适合注入依赖项和处理应用程序导航。看看这个post,对我帮助很大https://medium.com/@dkw5877/flow-coordinators-333ed64f3dd

据我了解,这些是您的要求:

  1. VC4 和 VC8 必须能够通过 UserService class.
  2. 共享状态
  3. UserService不能是单例。
  4. UserService 必须使用依赖注入提供给 VC4 和 VC8。
  5. 不得使用依赖注入框架。

在这些限制条件下,我建议采用以下方法。

定义一个 UserServiceProtocol,它具有用于访问和更新状态的方法 and/or 属性。例如:

protocol UserServiceProtocol {
    func login(user: String, password: String) -> Bool
    func logout()
    var loggedInUser: User? //where User is some model you define
}

定义一个 UserService class 来实现协议并将其状态存储在某处。

如果状态只需要持续到应用 运行,您可以将状态存储在特定的 实例 中,但是这个 instance 必须在 VC4 和 VC8 之间共享。

在这种情况下,我建议在 AppDelegate 中创建和保存实例,并通过 VC 链传递它。

如果状态需要在应用程序启动之间保持不变,或者如果您不想通过 VC 链传递实例,您可以将状态存储在用户默认值、Core Data、Realm 或任何class 本身之外的地方数。

在这种情况下,您可以在 VC3 和 VC7 中创建 UserService 并将其传递给 VC4 和 VC8。 VC4 和 VC8 会有 var userService: UserServiceProtocol?UserService 需要从外部源恢复其状态。这样即使VC4和VC8有不同的对象实例,状态也是一样的。

首先,我认为你的问题中存在错误的假设。

您这样定义 VC 的层次结构:

Example:

VC1 -> VC2 -> VC3 -> VC4

VC5 -> VC6 -> VC7 -> VC8

然而,在 iOS 上(除非你使用了一些非常奇怪的技巧)在某些时候总会有一个共同的父级,比如导航控制器、标签栏控制器、主从控制器或页面视图控制器。

所以我假设一个正确的方案可能看起来像这样:

Tab Bar Controller 1 -> Navigation Controller 1 -> VC1 -> VC2 -> VC3 -> VC4

Tab Bar Controller 1 -> Navigation Controller 2 -> VC5 -> VC6 -> VC7 -> VC8

我相信这样看可以很容易地回答您的问题。

现在,如果您要征求意见,在 iOS 上处理 DI 的最佳方式是什么,我会说没有最好的方式。然而,我个人喜欢坚持对象不应对自己负责的规则 creation/initialization。所以像

private lazy var service: SomeService = SomeService()

没有问题。我更喜欢需要 SomeService 实例或至少(对于 ViewControllers 来说很容易)的初始化:

var service: SomeService!

这样你就可以将获取正确 models/services 等的责任传递给实例的创建者,同时你可以通过一个简单但重要的假设来实现你的逻辑,即你拥有你需要的一切(或者你让你的 class 提前失败(例如通过使用强制解包),这在开发过程中实际上是好的。

现在,您如何获取这些模型 - 是通过初始化它们、传递它们、使用单例、使用提供者、容器、协调器等 - 这完全取决于您,并且还应该取决于诸如项目、客户需求、您使用的任何工具 - 所以一般来说,只要您坚持良好的 OOP 实践,任何有效的方法都可以。

这是我在几个项目中使用过的方法,可能会对您有所帮助。

  1. 通过 ViewControllerFactory 中的工厂方法创建所有视图控制器。
  2. ViewControllerFactory 有自己的 UserService 对象。
  3. 将 ViewControllerFactory 的 UserService 对象传递给那些需要它的视图控制器。

这里举个简单的例子:

struct ViewControllerFactory {

private let userService: UserServiceProtocol

init(userService: UserServiceProtocol) {
    self.userService = userService
}

// This VC needs the user service
func makeVC4() -> VC4 {
    let vc4 = VC4(userService: userService)
    return vc4
}

// This VC does not
func makeVC5() -> VC5 {
    let vc5 = VC5()
}

// This VC also needs the user service
func makeVC8() -> VC8 {
    let vc8 = VC8(userService: userService)
    return vc8
}
}  

ViewControllerFactory对象可以实例化并存储在AppDelegate中。

这是基础知识。此外,我还会查看以下内容(另请参阅此处提出一些好的建议的其他答案):

  1. 创建UserService 遵循的UserServiceProtocol。这使得创建用于测试的模拟对象变得容易。
  2. 研究协调器模式来处理导航逻辑。

我已尝试解决此问题并在此处上传了示例架构:https://github.com/ivanovi/DI-demo

为了更清楚,我使用三个 VC 简化了实现,但该解决方案适用于任何类型的深度。视图控制器链如下:

Master -> Detail -> MoreDetail(注入依赖的地方)

提议的架构有四个构建块:

  • 协调器存储库:包含所有协调器和共享状态。注入所需的依赖项。

  • ViewController 协调器:执行到下一个 ViewController 的导航。协调员持有一个工厂,该工厂生产 VC 所需的下一个实例。

  • ViewController工厂:负责初始化和配置特定的ViewController。它通常由协调器拥有,并由 CoordinatorRepository 注入到协调器中。

  • ViewController: ViewController要显示在屏幕上。

N.b.: 在示例中,我 return 新创建的 VC 实例只是为了生成示例 - 即在现实生活中实现 returning VC 不需要。

希望对您有所帮助。

let viewController = CustomViewController()
viewController.data = NSObject() //some data object
navigationController.show(viewController, sender: self)


import UIKit

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

    var window: UIWindow?
    var appCoordinator:AppCoordinator?

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
        // Override point for customization after application launch.
        window = UIWindow(frame: UIScreen.main.bounds)
        window?.rootViewController = UINavigationController()
        appCoordinator = AppCoordinator(with: window?.rootViewController as! UINavigationController)
        appCoordinator?.start()
        window?.makeKeyAndVisible()
        return true
    }
}