控制反轉、依賴注入、服務定位

原文鏈接

image

最近在閱讀兩款依賴注入的開源框架源碼——Swinject 和 Resolver,為了便于后續(xù)的源碼解讀,這里先寫一篇文章來梳理一下相關的概念,主要涉及控制反轉、依賴注入、服務定位等概念。

控制反轉

控制反轉(Inversion of Control,簡稱 IoC)是軟件開發(fā)中的一種設計思想,可以降低代碼間的耦合度。

這樣的解釋還是很抽象,那到底什么是控制反轉呢?下面,我們以一個例子來進行說明。

如下所示,我們定義了一個 Company 類,其依賴并創(chuàng)建了 Engineer 對象。我們把這種情況稱為 “控制正轉”,即 Company 在內部控制了對象的初始化、屬性賦值等操作。

protocol Employee {
    func work()
}

class Engineer: Employee {
    func work() {
        print("code")
    }
}

class Seller: Employee {
    func work() {
        print("sell")
    }
}

class Company {
    private let employee: Employee = Engineer()
    
    func operation() {
        employee.work()
    }
}

假如,我們希望 CompanySeller 進行工作,那該如何進行修改?按照上面的設計邏輯,我們會對 Company 作出如下修改。

class Company {
    private let employee: Employee = Seller()
    
    func operation() {
        employee.work()
    }
}

由于設計上的缺陷,當需求發(fā)生變更時,我們不得不對原來的類進行修改,這導致我們違反了 開閉原則——對擴展開放、對修改關閉。如下所示,為上述設計的類圖。

image

控制反轉的基本思想是:Companyemployee 的控制權從內部轉交至外部,由外部來控制對象的初始化、屬性賦值等操作。其期望的類圖如下所示,目標是移除 CompanyEmployeeImpl 實現類的依賴。這樣的話,當我們修改 EmployeeImpl 實現類的類型,也無需修改 Company 類的內部代碼,進而符合開閉原則。

image

通過一個例子,我們大致理解了 控制反轉 的概念。在實際開發(fā)中,常見的實現控制反轉的方式有兩種:

  • 依賴注入(Dependency Injection,簡稱 DI
  • 服務定位(Service Locator)

下面,我們分別對依賴注入和服務定位進行介紹。

依賴注入

依賴注入的基本思想是:通過一個注入器(Injector),由它來控制 Employee 的實現類的創(chuàng)建,并通過各種途徑注入到 Company。依賴注入的類圖如下所示。

注:在某些文章或框架中,注入器也被命名為容器(Containter或 IoC Container)

image

依賴注入的注入方式有以下幾種:

  • 構造器注入(Constructor Injection)
  • 屬性注入(Property Injection)
  • 方法注入(Method Injection)
  • 接口注入(Interface Injection)
  • 注解注入(Annotation Injection)

下面,我們依次進行介紹。

構造器注入

構造器注入是指通過初始化方法進行注入,如下所示。

class Company {
    private let employee: Employee

    init(employee: Employee) {
        self.employee = employee
    }

    func operation() {
        employee.work()
    }
}

class Injector {
    var employee: Employee = Engineer()
    lazy var company: Companny = Company(employee: employee)

    func test() {
        company.operation()
    }
}

屬性注入

屬性注入也稱為 setter 方法注入,即通過設置屬性的方式直接進行依賴注入,如下所示。

class Company {
    var employee: Employee?

    func operation() {
        employee?.work()
    }
}

class Injector {
    let employee: Employee = Engineer()
    let company: Company = Company()

    func test() {
        company.employee = employee
        company.operation()
    }
}

方法注入

方法注入則是通過方法參數傳入依賴,如下所示。屬性注入其實就是一種特殊的方法注入。

class Company {
    func operation(employee: Employee) {
        employee.work()
    }
}

class Injector {
    let employee: Employee = Engineer()
    let company: Company = Company()

    func test() {
        company.operation(employee: employee)
    }
}

接口注入

接口注入本質上和前面幾種依賴注入差不多,區(qū)別在于它需要為每種依賴聲明一個對應的接口,由使用方實現接口并注入依賴,如下所示。相比前幾種依賴注入,接口注入略微繁瑣。

protocol InjectEmployee {
    func inject(employee: Employee)
}

class Company {
    var employee: Employee?

    func operation() {
        employee?.work()
    }
}

extension Company: InjectEmployee {
    func inject(dependency: Employee) {
        employee = dependency
    }
}

class Injector {
    let employee: Employee = Engineer()
    let company: Company = Company()

    func test() {
        company.inject(employee: employee)
        company.operation()
    }
}

注解注入

注解注入則是通過在需要注入依賴的地方添加特定的注解,通過注解背后的實現,自動注入對應的依賴,如下所示。相對而言,注解注入是一種使用更加簡單的依賴注入方式。

@propertyWrapper
struct Injected {
    private var dependency: Employee = Engineer()

    var wrappedValue: Employee {
        get { return dependency }
    }
}

class Company {
    @Injected var employee: Employee

    func operation() {
        employee.work()
    }
}

class Tester {
    let company: Company = Company()

    func test() {
        company.operation()
    }
}

服務定位

服務定位,服務等同于被依賴的組件,定位即查找。服務定位的基本思想:一個服務定位器(Service Locator)持有所有服務,當 Company 需要 Employee 的實現類時,即可返回一個特定類型的實現類。服務定位的類圖如下所示。

image

從類圖中可以看出,服務定位與依賴注入的區(qū)別在于,服務定位只是將控制權從注入器轉移到了服務定位器中。在依賴注入中,Company 是被動注入依賴,Injector 依賴 Company;在服務定位中,Company 則是主動請求服務,Company 依賴 Injector。

從服務定位器的內部實現,可以將服務定位分為兩類:

  • 靜態(tài)服務定位
  • 動態(tài)服務定位

下面,依次進行介紹。

靜態(tài)服務定位

對于靜態(tài)服務定位,Service Locator 為每一種服務都提供了一個對應的方法,用于返回對應類型的服務,如下所示。

protocol Product {
    func use()
}

class AppForIOS: Product {
    func use() {
        print("iOS")
    }
}

class Company {
    private let employee: Employee = ServiceLocator().getEmployee()
    private let product: Product = ServiceLocator().getProduct()

    func operation() {
        employee.work()
        product.use()
    }
}

class ServiceLocator {
    func getEmployee() -> Employee {
        return Engineer()
    }

    func getProduct() -> Product {
        return AppForIOS()
    }
}

class Tester {
    let company: Company = Company()

    func test() {
        company.operation()
    }
}

當每次增加新的服務時,Service Locator 內部需要新增方法以支持新的服務類型。這種方式的缺點很明顯,因此現有的服務定位框架基本都是采用動態(tài)服務定位的方式設計實現的。

動態(tài)服務定位

動態(tài)服務定位會在內部維護一個哈希表,哈希表能夠存儲各種類型的服務,并提供泛型方法以支持返回不同類型的服務。當新增服務時,也無需對 Service Locator 進行修改。如下所示,是一個簡易的 Service Locator。

class Company {
    private let employee: Employee? = ServiceLocator.shared.resolve(type: Employee.self)
    private let product: Product? = ServiceLocator.shared.resolve(type: Product.self)

    func operation() {
        employee?.work()
        product?.use()
    }
}

class ServiceLocator {
    static let shared = ServiceLocator()

    private var map: [Int: Any] = [:]

    func register<Service>(type: Service.Type, service: Service) {
        let key = ObjectIdentifier(type.self).hashValue
        map[key] = service
    }

    func resolve<Service>(type: Service.Type) -> Service? {
        let key = ObjectIdentifier(type.self).hashValue
        return map[key] as? Service
    }
}


class Tester {
    func test() {
        ServiceLocator.shared.register(type: Employee.self, service: Engineer())
        ServiceLocator.shared.register(type: Product.self, service: AppForIOS())
        let company = Company()
        company.operation()
    }
}

依賴注入 vs 服務定位

依賴注入和服務定位兩者都實現了控制反轉的目的。兩者主要的區(qū)別在于提供服務(依賴)的方式不同。對于服務定位,使用方會顯式地向 Service Locator 發(fā)起請求,而依賴注入并沒有顯式請求,而是被動注入。

對于服務定位,每個服務的使用方都需要依賴 Service Locator。由于 Service Locator 能夠隱藏服務的相關信息,我們并不知道具體的依賴是什么,這可能會對于調試帶來一些困難。

對于依賴注入,由于我們知道各個顯式注入的位置,比如構造器,因此能夠沿著調用棧,找到依賴創(chuàng)建的地方,進一步知道依賴的具體信息。

總結

本文對控制反轉、依賴注入、服務定位等幾個概念,結合代碼進行了介紹。依賴注入根據注入的途徑又可以分為多種類型,包括:構造器注入、方法注入、屬性注入、接口注入、注解注入等。服務定位根據內部實現方式,可以分為靜態(tài)服務定位和動態(tài)服務定位兩種實現方式。依賴注入和服務定位雖然有些差異,但是兩者的目標是一致的,就是為了實現控制反轉。

整體上而言,控制反轉雖然能夠對代碼進行解耦,但是凡事都有兩面性。它會讓代碼邏輯變得更加復雜,調試也會更加困難。從這方面看來,關于是否采用控制反轉,需要具體場景具體分析,權衡利弊之后再作出選擇。

參考

  1. Inversion of Control Containers and the Dependency Injection pattern
  2. Resolver: Introduction
  3. 一文說透依賴注入
  4. Dependency Injection Strategies in Swift
  5. Swift中依賴注入的解耦策略
  6. Service Locator 模式
  7. iOS 組件通信方案
  8. Dependency Injection 101--What and Why
?著作權歸作者所有,轉載或內容合作請聯系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容