架构/设计

iOS开发中依赖注入Dependency Injection

2021-11-10  本文已影响0人  MambaYong

本文阅读时长45分钟,依赖注入DI是控制反转IOC的实现,通过依赖注入可以让代码实现松耦合,增强了代码的可扩展性和可维护性,同时也便于进行单元测试。

本文主要介绍一下内容:

控制反转和依赖注入

控制反转

控制反转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的接口就能实现不同音乐的播放,这里的接口就是抽象化的产物,实现了依赖的互换性。

依赖注入的几种方式

在依赖注入中通常会存在三个角色:

现在有一个代码如下:

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进行单元测试。

优点

缺点

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()

为了防止依赖没有注入时属性值为空,这里需要使用可选项,依赖采用属性赋值的方式进行了注入。

优点

缺点

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并能注入不同的依赖。

优点

缺点

依赖注入模式

依赖注入目前主要有三种模式,本文主要介绍的是Dependency Injection Container注入容器模式。

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")
        }
      })
    }
  }
}

代码解读:

主内容依赖对象

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())
  }
}

代码解读

隐私权限持久化存储

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)
  }
}

代码解读:

隐私页面构建

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
      }
    }
  }
}

代码解读:

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
  }
}

代码解读:

所有依赖对象的注册

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这种模式在项目中的实际运用,在项目中使用依赖注入能将代码松耦合,而且便于后期的维护,同时能很方便的进行单元测试,适合测试驱动开发(TDD)。

上一篇下一篇

猜你喜欢

热点阅读