快速的依赖注入与两个没有公共父级的UIViewControllers的依赖图

时间:2018-11-30 15:20:08

标签: ios swift dependency-injection inversion-of-control factory-pattern

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

示例:

VC1-> VC2-> VC3-> VC4

VC5-> VC6-> VC7-> VC8

让我们知道VC4和VC8都需要UserService来容纳当前用户。

请注意,我们要避免使用Singleton。

是否有一种优雅的方式来处理这种DI情况?

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

但是我找不到如何在iOS上应用该示例

7 个答案:

答案 0 :(得分:6)

好的,我会尝试一下。

您说的是“无单例”,因此在下面我将其排除在外,但也请参见此答案的底部。

Josh Homann的评论已经为一个解决方案提供了很好的指示,但是我个人对协调器模式有疑问。

正如乔什(Josh)正确说的,视图控制器之间不应该了解太多[1],但是例如协调员或传递/访问的任何依赖项?有几种模式可以建议如何使用,但是大多数模式都存在一个基本上与您的要求背道而驰的问题:它们或多或少使协调器成为一个单例(本身或作为另一个单例的属性,例如AppDelegate)。协调员通常也经常将一个人解因(但不总是如此,也不一定如此)。

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

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

lazy var userService: UserService = MyUserService()

我将其保持公开状态,因为这使我可以在单元测试中轻松地对其进行模拟/存根(如果需要这样做,我可以创建一个虚拟TestUserService来模拟/存根行为)。实例化也可以是一个闭包,如果init需要参数,我可以在测试期间轻松地将其关闭。显然,取决于对象的实际用途,甚至不必lazy属性。如果提前实例化对象没有任何危害(请记住单元测试,也要进行外发连接),只需跳过lazy

诀窍显然是设计UserService和/或MyUserService的方式在创建多个实例时不会导致问题强>。但是,我发现,只要实例被认为依赖的实际数据保存在其他地方,并且在一个事实中,例如钥匙串,核心数据栈,用户默认值或远程后端。

我知道这是一个解决之道,就像我只是在描述一种方法(至少是其中许多通用模式的一部分)那样。但是我发现这是Swift中进行依赖注入的最通用和最简单的形式。可以将协调器模式正交使用,但是我发现它在日常使用中不像“苹果般”。它确实解决了一个问题,但是大多数情况下,您不会正确使用情节提要(特别是:仅将其用作“ VC repos”,从那里实例化它们并在代码中进行转换)。

[1]除了一些基本的和/或次要的东西,您可以在完成处理程序或prepareForSegue中传递。这是有争议的,取决于您对协调员或其他模式的严格程度。就我个人而言,有时我会在这里采取捷径,只要它不会使事情things肿并且变得凌乱。这样一些弹出式设计就更简单了。


作为结束语,短语“请注意,我们希望避免使用Singleton”以及您对问题下的评论给我的印象是,您只是遵循该建议而没有适当考虑其理由。我知道“ Singleton”通常被认为是一种反模式,但同样经常这种判断是错误的。单例可能是一个有效的体系结构概念(您可以通过在框架和库中广泛使用它来了解这一点)。坏处在于,它常常会诱使开发人员采用设计捷径并将其滥用为一种“对象库”,从而使他们无需考虑何时何地实例化对象。这会导致混乱和图案的不良声誉。

一个UserService,取决于您的应用中实际执行的操作 可能是一个单例的好人选。我个人的经验法则是:“如果它管理的是单一且独特的事物的状态,例如特定用户在给定时间只能处于一种状态,那么我可能会单身

特别是如果您无法按照我上面概述的方式进行设计,即如果您需要内存中的奇异状态数据,那么单例基本上很容易且合适的实现方式。 (即使使用(惰性)属性也是有益的,然后您的视图控制器甚至不需要知道它是否是单例,并且您仍然可以单独对其进行存根/模拟(即,不仅是全局实例)。)

答案 1 :(得分: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类。

如果状态仅在应用程序运行时才需要持续,则可以将状态存储在特定的 instance 中,但是必须共享此 instance 在VC4和VC8之间。

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

如果状态需要在应用程序启动之间保持不变,或者您不想通过VC链传递实例,则可以将状态存储在用户默认设置,Core Data,Realm或任意数量的放在课程本身之外。

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

答案 2 :(得分:2)

首先,我认为您的问题有一个错误的假设。

您可以这样定义VC'c层次结构:

  

示例:

     

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实例或至少(对于ViewControllers而言很容易)的初始化:

var service: SomeService!

这样,您就可以将获取正确的模型/服务等的责任交给实例的创建者,同时,您可以通过简单但重要的假设来实现自己的逻辑,前提是您拥有所需的一切(或者您可以类过早失败(例如通过使用强制展开),这在开发过程中实际上是很好的。

现在,您如何获取这些模型-是通过初始化它们,传递它们,拥有一个单例,使用提供者,容器,协调器等来完成-这完全取决于您,并且还应取决于项目的复杂性等因素客户的需求,无论您使用什么工具-一般来说,只要您遵循良好的OOP惯例,任何可行的方法都可以。

答案 3 :(得分:2)

这是我在一些项目中可能会使用的一种方法。

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

这里是一个谦虚的例子:

daily_rainfall <- c(15, 2, 0, 0, 0, 3, 3, 0, 0, 10)

runs <- rle(daily_rainfall)
rainless_period_length <- runs$lengths[runs$values == 0]

rainless_period_length

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

这是基础。此外,我还将查看以下内容(另请参见此处提供了一些不错建议的其他答案):

  1. 创建UserService符合的UserServiceProtocol。这样可以轻松创建模拟对象进行测试。
  2. 查看协调器模式以处理导航逻辑。

答案 4 :(得分:0)

我发现协调器/路由器设计模式最适合注入依赖性和处理应用程序导航。看看这篇文章,对我有很大帮助https://medium.com/@dkw5877/flow-coordinators-333ed64f3dd

答案 5 :(得分:0)

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

为了更加清楚,我使用了三个VC简化了实现,但是该解决方案可以在任何深度进行工作。视图控制器链如下:

主->详细信息-> MoreDetail(在其中注入依赖项)

拟议的体系结构具有四个构建基块:

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

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

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

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

注:在示例中,我返回刚创建的VC实例只是为了生成示例-即,在现实生活中,不需要返回VC。

希望有帮助。

答案 6 :(得分: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
    }
}