使用Swift进行依赖注入,涉及两个没有共同父级的UIViewControllers的依赖图。

9
当我们有两个非常深层次的UIViewControllers,它们都需要持有状态的相同依赖项,而这两个UIViewControllers没有共同的父级时,我们如何在不使用框架的情况下应用依赖注入呢?
例如:
VC1 -> VC2 -> VC3 -> VC4
VC5 -> VC6 -> VC7 -> VC8
假设VC4和VC8都需要持有当前用户的UserService。
请注意,我们希望避免使用Singleton。
有没有一种优雅的方式来处理这种DI情况呢?
经过一些研究,我发现有人提到了"Abstract Factory"、"Context interfaces"、"Builder"和"strategy pattern"等方法。
但是我找不到在iOS上应用它们的示例。

1
VC1和VC5可能没有共同的父ViewController,但UserService可以在AppDelegate中创建并传递给它们两个。 - meggar
@meggar,但只有VC4和VC8需要使用UserService,所以在整个流程中传递它以仅在这两个子级上使用似乎对我来说不是最优的选择,特别是当它有两到三个依赖项时。 - iOSGeek
1
请注意,我们希望避免使用 Singleton。- 为什么? - mag_zbc
@mag_zbc更易于测试,避免使用全局变量... - iOSGeek
4
你应该有一个路由器/协调器,处理视图控制器之间的过渡并向每个视图控制器提供依赖。视图控制器之间不应相互了解,也不应该创建彼此,否则就会导致你描述的所有内容都需要知道和引用其他内容的情况。 - Josh Homann
显示剩余6条评论
7个回答

7

好的,我会尝试一下。

你说“不使用单例模式”,所以我在以下内容中排除了它,但请参阅答案底部。

Josh Homann的评论已经指出了一个解决方案,但是我个人对协调器模式有些困惑。

正如Josh所说,视图控制器不应该(太多地)相互了解[1],但是如何传递/访问依赖项,例如协调器?有几种模式可以解决这个问题,但大多数都存在一个问题,即违反了您的要求:它们更多地使协调器成为单例(自身或作为另一个单例的属性,如AppDelegate)。协调器通常也是事实上的单例(但并不总是,也没有必要这样)。

我倾向于依赖于简单初始化的属性或(最常见的)延迟属性和面向协议的编程。让我们构建一个示例:UserService将是定义服务所有功能的协议,MyUserService是其实现结构体。假设UserService是一个设计构造,基本上作为一种获取器/设置器系统用于某些用户相关数据:访问令牌(例如保存在钥匙串中),一些偏好设置(头像图像的URL)等。在初始化时,MyUserService还准备数据(例如从远程加载)。这将在多个独立的屏幕/视图控制器中使用,并且不是单例。

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

lazy var userService: UserService = MyUserService()

我将其公开,因为这样我可以轻松地在单元测试中进行模拟/存根(如果需要,我可以创建一个虚拟的TestUserService来模拟/存根行为)。实例化也可以是闭包,如果init需要参数,则可以在测试期间轻松切换。显然,根据对象实际执行的操作,属性甚至不一定需要是"lazy"。如果提前实例化对象不会有任何影响(考虑到单元测试和外部连接),则只需跳过"lazy"即可。
关键在于以一种不会在创建多个实例时导致问题的方式设计UserService和/或MyUserService。然而,我发现,在实际数据所依赖的实例保存在其他地方(如钥匙链、核心数据堆栈、用户默认设置或远程后端)的情况下,90%的时间这并不是真正的问题。
我知道这有点像是一个应付性的答案,因为某种程度上我只是描述了一种方法,它(至少部分地)是许多通用模式中的一种。然而,我发现这是使用Swift进行依赖注入最通用和简单的形式。协调器模式可以与之正交使用,但我发现在日常使用中它不太像苹果风格。它确实解决了一个问题,但主要是当你没有正确使用storyboard时(特别是:只将其用作"VC repos",从那里实例化它们并在代码中进行转换)。
除了一些基本和/或次要的事情,你可以通过完成处理程序或prepareForSegue来传递它们。这是有争议的,取决于你对协调器或其他模式的严格遵循程度。个人认为,在不使事情变得臃肿和混乱的情况下,我有时会走捷径。有些弹出设计用这种方式更简单。
总之,短语"请注意,我们要避免单例"以及你在问题下方的评论让我觉得你只是在遵循这个建议而没有正确地思考原理。我知道"Singleton"经常被认为是反模式,但同样经常是错误的判断。单例可能是一个有效的架构概念(这可以从它在框架和库中广泛使用中看出)。它的坏处仅仅在于它太容易引诱开发人员在设计上采取捷径,并滥用它作为一种"对象存储库",以便他们不需要考虑何时何地实例化对象。这会导致混乱和模式的不良声誉。
根据你的应用程序中UserService的实际操作,它可能是单例的一个很好的候选对象。我的个人经验法则是:"如果它管理某些唯一且独特状态的东西,比如一个特定的用户,那么该用户在任何给定时间只能处于一个状态",我可能会选择单例。

特别是如果您不能按照我上面概述的方式设计它,即如果您需要具有内存中的单个状态数据,那么单例基本上是一种简单且适当的实现方式。 (即使使用(惰性)属性也很有益处,您的视图控制器甚至不需要知道它是单例还是不是,您仍然可以分别进行存根/模拟(即不仅限于全局实例)。)


避免单例模式的响应加一。 - jacob bullock

3
以下是我理解的您的要求:
1. VC4 和 VC8 必须能够通过一个名为 UserService 的类共享状态。 2. UserService 不能是单例。 3. 使用依赖注入将 UserService 提供给 VC4 和 VC8。 4. 不得使用依赖注入框架。
在这些限制条件下,我建议采用以下方法。
定义一个 UserServiceProtocol,其中包含访问和更新状态的方法和/或属性。例如:
protocol UserServiceProtocol {
    func login(user: String, password: String) -> Bool
    func logout()
    var loggedInUser: User? //where User is some model you define
}

定义一个实现协议并在某处存储其状态的UserService类。
如果状态只需要持续到应用程序运行期间,您可以将状态存储在特定的实例中,但是这个实例必须在VC4和VC8之间共享。
在这种情况下,我建议在AppDelegate中创建并保持实例,并通过一系列的VC传递它。
如果状态需要在应用程序启动之间持久存在,或者您不想通过VC链传递实例,您可以将状态存储在用户默认设置、核心数据、领域或任何外部于类本身的地方。
在这种情况下,您可以在VC3和VC7中创建UserService并将其传递给VC4和VC8。VC4和VC8会有一个var userService: UserServiceProtocol?UserService需要从外部源恢复其状态。这样,即使VC4和VC8有不同的对象实例,状态也将相同。

2
首先,我认为你的问题有一个错误的假设。
你定义了你的VC层级关系如下:
示例: VC1 -> VC2 -> VC3 -> VC4 VC5 -> VC6 -> VC7 -> VC8
然而,对于iOS(除非你使用一些非常奇怪的黑科技),总会有一个共同的父级别,比如导航控制器、选项卡控制器、主细节控制器或页面视图控制器。
因此,我认为正确的方案可能看起来像这样:
选项卡控制器1 -> 导航控制器1 -> VC1 -> VC2 -> VC3 -> VC4 选项卡控制器1 -> 导航控制器2 -> VC5 -> VC6 -> VC7 -> VC8
我认为这样看问题很容易回答你的问题。
现在,如果你正在询问如何处理iOS上的DI的最佳方式,我想说并不存在一个最佳方式。但是,我个人喜欢遵循这样的规则,即对象不应该负责其自身的创建/初始化。所以像这样的东西:
private lazy var service: SomeService = SomeService()

这是不成问题的。我更喜欢需要 SomeService 实例的 init 或者至少(对于 ViewControllers 来说更容易):

var service: SomeService!

那样一来,你就把获取正确的模型/服务等的责任交给了实例的创建者,同时你可以使用一个简单但重要的假设来实现你的逻辑,即你拥有你需要的一切(或者你让你的类在早期失败(例如使用强制解包),这在开发过程中实际上是好的)。
现在,如何获取这些模型 - 是通过初始化它们、传递它们、使用单例、提供程序、容器、协调器等等 - 这完全取决于你,也应该取决于项目的复杂性、客户需求、你正在使用的工具等因素 - 所以通常,只要你遵循良好的面向对象编程实践,任何有效的方法都可以。

1

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

  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. 创建一个UserServiceProtocol,让UserService符合它。这样可以轻松地创建用于测试的模拟对象。
  2. 研究协调器模式来处理导航逻辑。

0

0

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

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

Master -> Detail -> MoreDetail(其中注入了依赖项)

所提出的架构有四个构建块:

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

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

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

  • ViewController:要在屏幕上呈现的ViewController。

N.b.:在示例中,我返回新创建的VC实例只是为了产生示例 - 即在实际实现中不需要返回VC。

希望能对您有所帮助。


0
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
    }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接