iOS开发中依赖注入Dependency Injection
本文阅读时长
45
分钟,依赖注入DI
是控制反转IOC
的实现,通过依赖注入可以让代码实现松耦合,增强了代码的可扩展性和可维护性,同时也便于进行单元测试。
本文主要介绍一下内容:
- 什么是控制反转?什么依赖注入?
-
iOS
开发中几种实现依赖注入的方式。 - 通过实际
Demo
演示依赖注入DI
在开发中的实际运用。
控制反转和依赖注入
控制反转
控制反转Inversion of Control(IOC)
不是一种技术,只是一种思想,一个重要的面向对象编程的法则,它能指导我们如何设计出松耦合、更优良的程序,简而言之就是让框架来掌控程序的执行流程,以完成类实例的创建和依赖关系的注入,听起来很抽象,还是结合例子来说明。
假设你是蝙蝠侠,你每天都从新闻记者阿尔弗雷德先生那里获得早上的晨报来了解哥谭的新闻,尽管你是蝙蝠侠,只要阿尔弗雷德先生休假你就无法看报纸了,问题就是蝙蝠侠看报纸是依赖阿尔弗雷德先生的,为了避免出现这个情况,你直接联系阿尔弗雷德先生的机构,即高谭出版社,为你提供报纸。在这种情况下你即可以通过阿尔弗雷德先生获得报纸,也可以通过该机构认为的任何其他代理人获得报纸,蝙蝠侠把送报的控制权从只依赖的个人反转到了报社。
struct Newspaper {
}
class NewspaperAgent {
let name: String
init(name: String) {
self.name = name
}
func giveNewspaper() -> Newspaper { }
}
struct HouseOwnerDetails {
let name: String
}
class House {
let newsPaperAgent: NewspaperAgent
let houseOwnerDetails: HouseOwnerDetails
init(houseOwnerDetails: HouseOwnerDetails, newsPaperAgent: NewspaperAgent) {
self.houseOwnerDetails = houseOwnerDetails
self.newsPaperAgent = newsPaperAgent
}
func startMorningActivities() {
let newsPaper = newsPaperAgent.giveNewspaper()
}
}
let houseOwnerDetail = HouseOwnerDetails(name: "Batman")
let newsPaperAgent = NewspaperAgent(name: "Alfred")
let wayneManor = House(houseOwnerDetails: houseOwnerDetail, newsPaperAgent: newsPaperAgent)
上面是阿尔弗雷德为蝙蝠侠在送报纸,蝙蝠侠看报纸得依赖阿尔弗雷德。
class House {
let newspaperAgency: NewsAgentProvidable
let houseOwnerDetails: HouseOwnerDetails
init(houseOwnerDetails: HouseOwnerDetails, newspaperAgency: NewsAgentProvidable) {
self.houseOwnerDetails = houseOwnerDetails
self.newspaperAgency = newspaperAgency
}
func startMorningActivities() {
let newspaper = newspaperAgency.getNewsAgent(for: houseOwnerDetails).giveNewsaper()
}
}
protocol NewsAgentProvidable {
func getNewsAgent(for ownerDetails: HouseOwnerDetails) -> NewsaperAgent
}
class NewsAgency: NewsAgentProvidable {
let name: String
var agents: [NewsaperAgent] = []
init(name: String) {
self.name = name
}
func getNewsAgent(for ownerDetails: HouseOwnerDetails) -> NewsaperAgent {
// Get a news agent
}
}
let houseOwnerDetail = HouseOwnerDetails(name: "Batman")
let agency = NewsAgency(name: "Gotham Publications")
let wayneManor = House(houseOwnerDetails: houseOwnerDetail, newspaperAgency: agency)
现在蝙蝠侠要看报纸就不用找阿尔弗雷德,可以直接说“喂,是哥谭报社吗,我想要一份晨报”,这时报社就会安排人将报纸送来,当某个快送员请假时就可以安排其他人继续送,这样就消除了蝙蝠侠与阿尔弗雷德直接的依赖关系,在编程中体现为松耦合。
基于控制反转的理念,在编程中一个类只负责其主要的职责,其他的事情需要移到外面去并与他们形成依赖关系,不用在类的内部直接形成依赖,通过抽象化可以实现依赖的互换性,实现控制反转有很多种方式,其中依赖注入DI
就是实现控制反转的一种。
依赖注入
当一个classA
请求它的environment
来加载另外一个classB
,这样无法直接让classA
使用另外一个classC
,通俗的讲就是无法随意的更换合作者,这样导致单元测试无法进行,一旦项目庞大,代码的可维护性和可扩展性就很低,其实上面蝙蝠侠的例子已经使用了依赖性注入。
以前音乐盒能播放的音乐都刻在了鼓上,要想听不同的音乐只能更换股,音乐盒为
classA
,内部的classB
为鼓;现在的iPod
则只需要一个USB
的接口就能实现不同音乐的播放,这里的接口就是抽象化的产物,实现了依赖的互换性。
依赖注入的几种方式
在依赖注入中通常会存在三个角色:
-
Injector : 实现依赖关系并与
Client
连接。 -
Dependency : 被
Client
注入的依赖。 - Client : 因功能完整需要注入依赖的那个类。
现在有一个代码如下:
struct DenpendencyImplementation {
func foo(){
// Does something
}
}
class Client {
init() {
let denpendency = DenpendencyImplementation()
denpendency.foo()
}
}
let client = Client()
上面这段代码很明显Client
在内部依赖了denpendency
,在类里创建了实例,只要初始化Client
时就会调用foo
这个方法,试想一下如何只对Client
这个类进行单元测试?因为denpendency
和Client
已经耦合在一起了,单元测试变得异常困难,为此需要引入依赖注入。
Constructor Injection
Constructor Injection
注入是最常用的一种方式,直接将依赖关系通过构造函数的参数进行注入:
protocol Dependency {
func foo()
}
struct DependencyImplementation: Dependency {
func foo() {
// Does something
}
}
class Client {
let dependency: Dependency
init(dependency: Dependency) {
self.dependency = dependency
}
func foo() {
dependency.foo()
}
}
let client = Client(dependency: DependencyImplementation())
client.foo()
上面代码中用构造函数的参数将dependency
职责分离分出,并且利用协议进行了抽象化,这样只需要符合Dependency
协议的依赖都能初始化Client
,同时利用Dependency
协议可以生成一个Mock
的denpendency
来注入到Client
进行单元测试。
优点:
- 对封装极为友好。
- 保证
Client
总是处于完整的状态。
缺点:
- 依赖注入后时无法在改变。
- 当超过3个依赖时,构造函数将会因参数过多而狠恶心🤢 。
Setter Injection
Setter Injection
这是其他语言所说的属性注入或者方法注入,利用属性赋值的方式注入:
protocol Dependency {
func foo()
}
struct DependencyImplementation: Dependency {
func foo() {
// Does something
}
}
class Client {
var dependency: Dependency!
func foo() {
dependency.foo()
}
// 或者调用此方法给属性赋值
func setDenpendency(denpendency:Dependency) {
self.denpendency = denpendency
}
}
let client = Client()
client.dependency = DependencyImplementation()
client.foo()
为了防止依赖没有注入时属性值为空,这里需要使用可选项,依赖采用属性赋值的方式进行了注入。
优点:
- 可以初始化
Client
之后在进行依赖的注入。 - 利用可读的属性可以注入具有多个依赖关系的对象,非常方便。
缺点:
- 由于是属性注入在封装时不太友好。
- 当未注入依赖时或者忘记注入依赖时
Client
将出去欠缺状态。 - 必须得使用可选项属性
Interface Injection
依赖通常通过属性注入的方式注入,由Injector
统一来处理不同类型的Client
,并且Injector
可以运用不同的策略在Client
上,听起来十分抽象,还是上代码:
protocol Dependency {}
protocol HasDependency {
func setDependency(_ dependency: Dependency)
}
protocol DoesSomething {
func doSomething()
}
class Client: HasDependency, DoesSomething {
private var dependency: Dependency!
func setDependency(_ dependency: Dependency) {
self.dependency = dependency
}
func doSomething() {
// Does something with a dependency
}
}
class Injector {
typealias Client = HasDependency & DoesSomething
private var clients: [Client] = []
func inject(_ client: Client) {
clients.append(client)
client.setDependency(SomeDependency())
// Dependency applies its policies over clients
client.doSomething()
}
// Switch dependencies under certain conditions
func switchToAnotherDependency() {
clients.forEach { $0.setDependency(AnotherDependency()) }
}
}
class SomeDependency: Dependency {}
class AnotherDependency: Dependency {}
依靠Client
遵守HasDependency
和DoesSomething
二个协议来实现不同的行为,当然这里HasDependency
的协议只是用方法注入来给Client
注入依赖,其实还可以是其它实现;Injector
中的Inject
方法给不同类型(如何实现二个协议)的注入SomeDependency
这个依赖,而switchToAnotherDependency
这个方法则注入的是AnotherDependency
这个依赖,这样就实现了Injector
负责处理不容类型的Client
并能注入不同的依赖。
优点:
- 同样支持初始化
Client
之后在进行依赖的注入。 -
Injector
可以根据不同类型的Cilent
注入不同的依赖。 -
Injector
可以根据Client
实现协议的不同实现不同类型的Client
。
缺点:
- 仔细看
Client
其实都成了Injector
的依赖了。
依赖注入模式
依赖注入目前主要有三种模式,本文主要介绍的是Dependency Injection Container
注入容器模式。
- Factory
- Dependency Injection Container
- Service Locator
Dependency Injection Container
简称DI Container
主要用来注册和解决项目中的所有依赖关系,管理依赖对象的生命周期以及在需要的时候自动进行依赖注入。
项目实战
项目演示采用了
swiftUI
,最终效果如上图所示,通过Privacy preferences
页面选择相应的隐私权限级别来控制个人profile
主界面的相关个人信息模块的展示,下面会贴出主要代码,具体Demo
传送门在此。
界面的搭建
import SwiftUI
struct ProfileView<ContentProvider>: View where ContentProvider: ProfileContentProviderProtocol {
private let user: User
// 2 利用Combine实现响应式
@ObservedObject private var provider: ContentProvider
// 1 采用构造方法的注入方式进行依赖对象的注入,同时依赖对象从容器中统一获取
init(provider: ContentProvider = DIContainer.shared.resolve(type: ContentProvider.self)!, user: User = DIContainer.shared.resolve(type: User.self)!) {
self.provider = provider
self.user = user
}
var body: some View {
NavigationView {
ScrollView(.vertical, showsIndicators: true) {
VStack {
ProfileHeaderView(
user: user,
canSendMessage: provider.canSendMessage,
canStartVideoChat: provider.canStartVideoChat
)
provider.friendsView
provider.photosView
provider.feedView
}
}
.navigationTitle("Profile")
.navigationBarItems(trailing: Button(action: {}){
NavigationLink(destination: UserPreferencesView<PreferencesStore>()){
Image(systemName: "gear")
}
})
}
}
}
代码解读:
-
ProfileView
采用了构造方法来注入User
实例和满足ProfileContentProviderProtocol
协议的依赖对象,此ProfileContentProviderProtocol
协议则是上面我们提到的抽象封装,只要满足此协议的对象都能注入到ProfileView
中,同时二个依赖对象由DIContainer
统一进行调配。 -
provider
这个依赖对象使用了@ObservedObject
这个属性包装器,当其相关属性值发生改变时,swiftUI
会及时刷新UI
保持ProfileHeaderView
,friendsView
,feedView
和photosView
为最新状态。
主内容依赖对象
import Foundation
import SwiftUI
import Combine
// 利用协议进行了依赖对象的抽象化提取,只要满足协议的对象都能作为依赖对象注入
protocol ProfileContentProviderProtocol: ObservableObject {
var privacyLevel: PrivacyLevel { get }
var canSendMessage: Bool { get }
var canStartVideoChat: Bool { get }
var photosView: AnyView { get }
var feedView: AnyView { get }
var friendsView: AnyView { get }
}
// 遵守协议的依赖对象
final class ProfileContentProvider<Store>: ProfileContentProviderProtocol where Store: PreferencesStoreProtocol{
let privacyLevel: PrivacyLevel
private let user: User
private var store: Store
private var cancellables: Set<AnyCancellable> = []
// 1 依赖对象内部也采用了构造方法的注入
init(privacyLevel: PrivacyLevel = DIContainer.shared.resolve(type: PrivacyLevel.self)!, user: User = DIContainer.shared.resolve(type: User.self)!,
store: Store = DIContainer.shared.resolve(type: Store.self)!) {
self.privacyLevel = privacyLevel
self.user = user
self.store = store
// 2 订阅事件
store.objectWillChange.sink{_ in
self.objectWillChange.send()
}
.store(in: &cancellables)
}
var canSendMessage: Bool {
privacyLevel >= store.messagePreference
}
var canStartVideoChat: Bool {
privacyLevel >= store.videoCallsPreference
}
var photosView: AnyView {
privacyLevel >= store.photosPreference ?
AnyView(PhotosView(photos: user.photos)) :
AnyView(EmptyView())
}
var feedView: AnyView {
privacyLevel >= store.feedPreference ?
AnyView(HistoryFeedView(posts: user.historyFeed)) :
AnyView(RestrictedAccessView())
}
var friendsView: AnyView {
privacyLevel >= store.friendsListPreference ?
AnyView(UsersView(title: "Friends", users: user.friends)) :
AnyView(EmptyView())
}
}
代码解读
-
ProfileContentProvider
内部也同样用构造方法注入了PrivacyLevel
,User
和Store
实例的依赖对象,依赖对象同样由DIContainer
统一进行调配。 -
store
实例订阅了事件,当store
进行了持久化存储的改变时会受收到事件,并让遵守ObservableObject
协议的ProfileContentProvider
发出事件,以便ProfileView
收到事件后刷新UI
。 -
canSendMessage
,canStartVideoChat
,photosView
,feedView
和friendsView
全部采用了计算属性进行定义,根据传入进来的依赖对象进行属性值的设置。
隐私权限持久化存储
import Combine
import Foundation
protocol PreferencesStoreProtocol: ObservableObject {
var friendsListPreference: PrivacyLevel { get set }
var photosPreference: PrivacyLevel { get set }
var feedPreference: PrivacyLevel { get set }
var videoCallsPreference: PrivacyLevel { get set }
var messagePreference: PrivacyLevel { get set }
func resetPreferences()
}
final class PreferencesStore: PreferencesStoreProtocol {
// 1 遵守了ObservableObject需要用@Published指明需要发布的属性
@Published var friendsListPreference = value(for: .friends, defaultValue: .friend) {
// 2 属性观察器
didSet {
set(value: photosPreference, for: .friends)
}
}
@Published var photosPreference = value(for: .photos, defaultValue: .friend) {
didSet {
set(value: photosPreference, for: .photos)
}
}
@Published var feedPreference = value(for: .feed, defaultValue: .friend) {
didSet {
set(value: feedPreference, for: .feed)
}
}
@Published var videoCallsPreference = value(for: .videoCall, defaultValue: .closeFriend) {
didSet {
set(value: videoCallsPreference, for: .videoCall)
}
}
@Published var messagePreference: PrivacyLevel = value(for: .message, defaultValue: .friend) {
didSet {
set(value: messagePreference, for: .message)
}
}
func resetPreferences() {
let defaults = UserDefaults.standard
PrivacySetting.allCases.forEach { setting in
//forEach注意return的问题
defaults.removeObject(forKey: setting.rawValue)
}
}
// 本地持久化存储
private static func value(for key: PrivacySetting, defaultValue: PrivacyLevel) -> PrivacyLevel {
let value = UserDefaults.standard.string(forKey: key.rawValue) ?? ""
return PrivacyLevel.from(string: value) ?? defaultValue
}
private func set(value: PrivacyLevel, for key: PrivacySetting) {
UserDefaults.standard.setValue(value.title, forKey: key.rawValue)
}
}
代码解读:
-
PreferencesStore
利用UserDefaults
提供的重置,设置和取值三个方法用来持久化个人的隐私设置。 -
PreferencesStore
遵守ObservableObject
协议,采用了@Published
来对需要发布的属性进行包装。并采用了属性观察进行持久化的存储。
隐私页面构建
import SwiftUI
import Combine
struct UserPreferencesView<Store>: View where Store: PreferencesStoreProtocol {
private var store: Store
// 1 构造方法注入
init(store: Store = DIContainer.shared.resolve(type: Store.self)!) {
self.store = store
}
var body: some View {
NavigationView {
VStack {
PreferenceView(title: .photos, value: store.photosPreference) { value in
// 2 触发属性观察,进行持久化存储并利用@published发布事件
PreferenceView(title: .friends, value: store.friendsListPreference) { value in
store.friendsListPreference = value
}
PreferenceView(title: .feed, value: store.feedPreference) { value in
store.feedPreference = value
}
PreferenceView(title: .videoCall, value: store.videoCallsPreference) { value in
store.videoCallsPreference = value
}
PreferenceView(title: .message, value: store.messagePreference) { value in
store.messagePreference = value
}
Spacer()
}
}.navigationBarTitle("Privacy preferences")
}
}
struct PreferenceView: View {
private let title: PrivacySetting
private let value: PrivacyLevel
// 3 点击按钮执行的闭包
private let onPreferenceUpdated: (PrivacyLevel) -> Void
init(title: PrivacySetting, value: PrivacyLevel, onPreferenceUpdated: @escaping (PrivacyLevel) -> Void) {
self.title = title
self.value = value
self.onPreferenceUpdated = onPreferenceUpdated
}
var body: some View {
HStack {
Text(title.rawValue).font(.body)
Spacer()
PreferenceMenu(title: value.title, onPreferenceUpdated: onPreferenceUpdated)
}.padding()
}
}
struct PreferenceMenu: View {
@State var title: String
private let onPreferenceUpdated: (PrivacyLevel) -> Void
init(title: String, onPreferenceUpdated: @escaping (PrivacyLevel) -> Void) {
_title = State<String>(initialValue: title)
self.onPreferenceUpdated = onPreferenceUpdated
}
var body: some View {
Menu(title) {
Button(PrivacyLevel.closeFriend.title) {
onPreferenceUpdated(PrivacyLevel.closeFriend)
title = PrivacyLevel.closeFriend.title
}
Button(PrivacyLevel.friend.title) {
onPreferenceUpdated(PrivacyLevel.friend)
title = PrivacyLevel.friend.title
}
Button(PrivacyLevel.everyone.title) {
onPreferenceUpdated(PrivacyLevel.everyone)
title = PrivacyLevel.everyone.title
}
}
}
}
代码解读:
- 同样采用构造方法来注入
store
这个持久化存储的实例,依然是DIContainer
统一调配。 - 按钮点击后利用闭包回传,将隐私的值利用
store
进行持久化的存储,并利用@published
进行事件的发布。
DIContainer注入容器的创建
import Foundation
protocol DIContainerProtocol {
func register<Component>(type: Component.Type, component: Any)
func resolve<Component>(type: Component.Type) -> Component?
}
final class DIContainer: DIContainerProtocol {
// 采用单例模式
static let shared = DIContainer()
// 禁止外界使用init初始化
private init() {}
// 用字典保存依赖对象
var components: [String: Any] = [:]
func register<Component>(type: Component.Type, component: Any) {
// 注册
components["\(type)"] = component
}
func resolve<Component>(type: Component.Type) -> Component? {
// 取出准备注入
return components["\(type)"] as? Component
}
}
代码解读:
- 采用单利模式创建
DIContainer
,并将init
初始化方法设为private
防止外界调用init方法进行初始化。 - 利用字典来将注册过后的依赖对象进行存储或者取出。
所有依赖对象的注册
import UIKit
import SwiftUI
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
typealias Provider = ProfileContentProvider<PreferencesStore>
//这里是在进行依赖注入对象的初始化,利用容器进行注册
let container = DIContainer.shared
container.register(type: PrivacyLevel.self, component: PrivacyLevel.friend)
container.register(type: User.self, component: Mock.user())
container.register(type: PreferencesStore.self, component: PreferencesStore())
container.register(
type: Provider.self,
component: Provider())
let profileView = ProfileView<Provider>()
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
window.rootViewController = UIHostingController(rootView: profileView)
self.window = window
window.makeKeyAndVisible()
}
}
}
代码解读:
- 在使用依赖对象之前统一用
DIContainer
单例将上面所有代码所有用到的依赖对象进行注册即可。 - 在用到依赖对象的地方,从
DIContainer
取出依赖对象注入即可。
总结:
只要介绍了控制反转的思想,同时对依赖注入的几种方式和模式进行了介绍,并比较了其优缺点,并演示了DIContainer
这种模式在项目中的实际运用,在项目中使用依赖注入能将代码松耦合,而且便于后期的维护,同时能很方便的进行单元测试,适合测试驱动开发(TDD
)。