具有swift的依赖注入,具有两个UIViewControllers的依赖图,没有公共父级



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

示例:

VC1->VC2->VC3->VC4-

VC5->VC6->VC7->VC8-

让我们假设VC4和VC8都需要持有当前用户的CCD_ 1。

请注意,我们希望避免辛格尔顿。

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

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

但我找不到如何在iOS 上应用它的例子

好的,我来试试。

你说"没有单身",所以我在下面排除了这一点,但也请看这个答案的底部。

Josh Homann的评论已经是一个很好的解决方案,但就我个人而言,我对协调器模式有一些问题。

正如Josh正确地说的那样,视图控制器不应该彼此了解(太多)[1],但协调器或任何依赖关系是如何传递/访问的?有几种模式提出了如何实现的建议,但大多数模式都有一个基本上违背您需求的问题:它们或多或少地使协调器成为一个单例(要么是它本身,要么是像AppDelegate这样的另一个单体的属性)。协调器通常也会减少单例的因素(但并不总是如此,也不一定非得如此)。

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

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

lazy var userService: UserService = MyUserService()

我将其公开,因为这允许我在单元测试中轻松地模拟/存根它(如果需要,我可以创建一个模拟/存根行为的伪TestUserService)。实例化也可以是一个闭包,如果init需要参数,我可以在测试期间轻松切换它。显然,属性甚至不一定需要是lazy,这取决于对象的实际操作。如果提前实例化对象没有害处(记住单元测试,还有传出连接),只需跳过lazy

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

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

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


作为结束语,短语"注意,我们希望避免辛格尔顿"以及您在该问题下对此的评论给我的印象是,您只是遵循了该建议,而没有适当考虑其理由。我知道"辛格尔顿"经常被认为是一种反模式,但同样常见的是,这种判断是错误的。单例可以是一个有效的体系结构概念(您可以从它在框架和库中广泛使用的事实中看到这一点)。它的糟糕之处在于,它经常诱使开发人员在设计中走捷径,并将其滥用为"对象存储库",这样他们就不需要考虑何时何地实例化对象。这导致了图案的混乱和坏名声。

UserService,取决于它在应用程序中的实际作用,可能是一个很好的单例候选者。我个人的经验法则是:"如果它管理某个奇异而独特的东西的状态,比如一个特定的用户在给定的时间只能处于一个状态",我可能会选择singleton。

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

据我所知,这些是您的需求:

  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链传递实例,你可以将该状态存储在用户默认值、核心数据、Realm或类本身之外的任何数量的位置。

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

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

您将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!

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

现在,你如何获取这些模型——是通过初始化它们,传递它们,拥有一个单例,使用提供者、容器、协调器等等——这完全取决于你,也应该取决于项目的复杂性、客户需求、你使用的任何工具等因素——所以一般来说,只要你坚持良好的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. 查看Coordinator模式以处理导航逻辑

我发现协调器/路由器设计模式最适合注入依赖关系和处理应用程序导航。看看这个帖子,它帮了我很多https://medium.com/@dkw5877/流量协调器-333ed64f3dd

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

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

Master->Detail->MoreDetail(注入依赖项的位置)

提议的体系结构有四个构建块:

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

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

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

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

N.b.:在这个例子中,我返回新创建的VC实例只是为了生成这个例子——也就是说,在现实生活中,不需要返回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
}
}

最新更新