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<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保持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<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內部也同樣用構造方法注入了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<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)。

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容