当我们有两个层次结构很深的UIViewController且它们都需要拥有状态的相同依赖项并且这两个UIViewController没有共同的父级时,如何在不使用Framework的情况下应用依赖注入。
示例:
VC1-> VC2-> VC3-> VC4
VC5-> VC6-> VC7-> VC8
让我们知道VC4和VC8都需要UserService
来容纳当前用户。
请注意,我们要避免使用Singleton。
是否有一种优雅的方式来处理这种DI情况?
经过研究,我发现有人提到Abstract Factory
,Context interfaces
,Builder
,strategy pattern
但是我找不到如何在iOS上应用该示例
答案 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)
据我了解,这是您的要求:
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
类。
如果状态仅在应用程序运行时才需要持续,则可以将状态存储在特定的 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)
这是我在一些项目中可能会使用的一种方法。
这里是一个谦虚的例子:
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中。
这是基础。此外,我还将查看以下内容(另请参见此处提供了一些不错建议的其他答案):
答案 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
}
}