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这个类进行单元测试?因为denpendencyClient已经耦合在一起了,单元测试变得异常困难,为此需要引入依赖注入。

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协议可以生成一个Mockdenpendency来注入到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遵守HasDependencyDoesSomething二个协议来实现不同的行为,当然这里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: 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()){
          Image(systemName: "gear")
        }
      })
    }
  }
}

代码解读:

  • ProfileView采用了构造方法来注入User实例和满足ProfileContentProviderProtocol协议的依赖对象,此ProfileContentProviderProtocol协议则是上面我们提到的抽象封装,只要满足此协议的对象都能注入到ProfileView中,同时二个依赖对象由DIContainer统一进行调配。
  • provider这个依赖对象使用了@ObservedObject 这个属性包装器,当其相关属性值发生改变时,swiftUI会及时刷新UI保持ProfileHeaderViewfriendsViewfeedViewphotosView为最新状态。

主内容依赖对象

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: ProfileContentProviderProtocol where Store: PreferencesStoreProtocol{
  let privacyLevel: PrivacyLevel
  private let user: User
  private var store: Store
  private var cancellables: Set = []
  // 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内部也同样用构造方法注入了PrivacyLevelUserStore实例的依赖对象,依赖对象同样由DIContainer统一进行调配。
  • store实例订阅了事件,当store进行了持久化存储的改变时会受收到事件,并让遵守ObservableObject协议的ProfileContentProvider发出事件,以便ProfileView收到事件后刷新UI
  • canSendMessagecanStartVideoChatphotosViewfeedViewfriendsView全部采用了计算属性进行定义,根据传入进来的依赖对象进行属性值的设置。

隐私权限持久化存储

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: 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(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(type: Component.Type, component: Any)
  func resolve(type: Component.Type) -> Component?
}

final class DIContainer: DIContainerProtocol {
  // 采用单例模式
  static let shared = DIContainer()
  
  // 禁止外界使用init初始化
  private init() {}

  // 用字典保存依赖对象
  var components: [String: Any] = [:]

  func register(type: Component.Type, component: Any) {
    // 注册
    components["\(type)"] = component
  }

  func resolve(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
    //这里是在进行依赖注入对象的初始化,利用容器进行注册
    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()
    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)。

你可能感兴趣的:(iOS开发中依赖注入Dependency Injection)