当我们有两个层次结构中很深的UIViewController,并且它们都需要保持状态的相同依赖项,而这两个UIViewController没有共同的父级时,我们如何在不使用框架的情况下应用依赖项注入。
示例:
VC1->VC2->VC3->VC4-
VC5->VC6->VC7->VC8-
让我们假设VC4和VC8都需要持有当前用户的CCD_ 1。
请注意,我们希望避免辛格尔顿。
有没有一种优雅的方法来处理这种DI情况?
经过一些研究,我发现有些提到了Abstract Factory
、Context interfaces
、Builder
、strategy pattern
但我找不到如何在iOS 上应用它的例子
好的,我来试试。
你说"没有单身",所以我在下面排除了这一点,但也请看这个答案的底部。
Josh Homann的评论已经是一个很好的解决方案,但就我个人而言,我对协调器模式有一些问题。
正如Josh正确地说的那样,视图控制器不应该彼此了解(太多)[1],但协调器或任何依赖关系是如何传递/访问的?有几种模式提出了如何实现的建议,但大多数模式都有一个基本上违背您需求的问题:它们或多或少地使协调器成为一个单例(要么是它本身,要么是像AppDelegate
这样的另一个单体的属性)。协调器通常也会减少单例的因素(但并不总是如此,也不一定非得如此)。
我倾向于依赖简单初始化属性或(最常见的)懒惰属性和面向协议的编程。让我们构造一个例子:UserService
应该是定义服务所需的所有功能的协议,MyUserService
是它的实现结构。让我们假设UserService
是一个设计结构,它基本上充当一些用户相关数据的getter/setter系统:访问令牌(例如保存在钥匙链中)、一些偏好(化身图像的URL)等等。在初始化时,UserService
0还准备数据(例如从远程加载)。这将在几个独立的屏幕/视图控制器中使用,并且不是单一的。
现在,每个有兴趣访问这些数据的视图控制器都有一个简单的属性:
lazy var userService: UserService = MyUserService()
我将其公开,因为这允许我在单元测试中轻松地模拟/存根它(如果需要,我可以创建一个模拟/存根行为的伪TestUserService
)。实例化也可以是一个闭包,如果init需要参数,我可以在测试期间轻松切换它。显然,属性甚至不一定需要是lazy
,这取决于对象的实际操作。如果提前实例化对象没有害处(记住单元测试,还有传出连接),只需跳过lazy
。
诀窍显然是设计UserService
和/或MyUserService
,在创建多个实例时不会出现问题。然而,我发现,只要实例应该依赖的实际数据保存在其他地方,比如钥匙链、核心数据堆栈、用户默认值或远程后端,90%的情况下这并不是一个真正的问题。
我知道这是一种逃避的回答,因为在某种程度上,我只是在描述一种方法,它(至少是)是许多通用模式的一部分。然而,我发现这是Swift中最通用、最简单的依赖注入方式。协调器模式可以与之正交使用,但我发现它在日常使用中不那么"像苹果"。它确实解决了一个问题,但大多数情况下,你没有按照预期正确使用故事板(尤其是:只是将它们用作"VC repo",从那里实例化它们,并在代码中转换自己)。
[1] 除了一些基本的和/或次要的东西,您可以在完成处理程序或prepareForSegue
中传递。这是有争议的,取决于你对协调员或其他模式的严格程度。就我个人而言,我有时会在这里走捷径,只要它不会把东西弄得一团糟。有些弹出式设计更简单。
作为结束语,短语"注意,我们希望避免辛格尔顿"以及您在该问题下对此的评论给我的印象是,您只是遵循了该建议,而没有适当考虑其理由。我知道"辛格尔顿"经常被认为是一种反模式,但同样常见的是,这种判断是错误的。单例可以是一个有效的体系结构概念(您可以从它在框架和库中广泛使用的事实中看到这一点)。它的糟糕之处在于,它经常诱使开发人员在设计中走捷径,并将其滥用为"对象存储库",这样他们就不需要考虑何时何地实例化对象。这导致了图案的混乱和坏名声。
UserService
,取决于它在应用程序中的实际作用,可能是一个很好的单例候选者。我个人的经验法则是:"如果它管理某个奇异而独特的东西的状态,比如一个特定的用户在给定的时间只能处于一个状态",我可能会选择singleton。
特别是如果你不能按照我上面概述的方式设计它,即如果你需要在内存中有奇异状态数据,单例基本上是一种简单且合适的实现方式。(即使使用(惰性)属性也是有益的,您的视图控制器甚至不需要知道它是否是单例,而且您仍然可以单独对其进行存根/模拟(即,不仅仅是全局实例)。)
据我所知,这些是您的需求:
- VC4和VC8必须能够通过
UserService
类共享状态 UserService
不能是单例- 必须使用依赖注入将
UserService
提供给VC4和VC8 - 不能使用依赖项注入框架
在这些限制条件下,我建议采用以下方法。
定义具有用于访问和更新状态的方法和/或属性的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实践,任何有效的方法都是好的。
以下是我在一些项目中使用的一种方法,可能会对您有所帮助。
- 通过ViewControllerFactory中的工厂方法创建所有视图控制器
- ViewControllerFactory有自己的UserService对象
- 将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中。
这是最基本的。此外,我还将研究以下内容(另请参阅其他给出了一些好建议的答案):
- 创建一个UserService符合的UserServiceProtocol。这使得创建用于测试的模拟对象变得容易
- 查看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
}
}