最近在閱讀兩款依賴注入的開源框架源碼——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()
}
}
假如,我們希望 Company 由 Seller 進行工作,那該如何進行修改?按照上面的設計邏輯,我們會對 Company 作出如下修改。
class Company {
private let employee: Employee = Seller()
func operation() {
employee.work()
}
}
由于設計上的缺陷,當需求發(fā)生變更時,我們不得不對原來的類進行修改,這導致我們違反了 開閉原則——對擴展開放、對修改關閉。如下所示,為上述設計的類圖。
控制反轉的基本思想是:把 Company 對 employee 的控制權從內部轉交至外部,由外部來控制對象的初始化、屬性賦值等操作。其期望的類圖如下所示,目標是移除 Company 對 EmployeeImpl 實現類的依賴。這樣的話,當我們修改 EmployeeImpl 實現類的類型,也無需修改 Company 類的內部代碼,進而符合開閉原則。
通過一個例子,我們大致理解了 控制反轉 的概念。在實際開發(fā)中,常見的實現控制反轉的方式有兩種:
- 依賴注入(Dependency Injection,簡稱 DI)
- 服務定位(Service Locator)
下面,我們分別對依賴注入和服務定位進行介紹。
依賴注入
依賴注入的基本思想是:通過一個注入器(Injector),由它來控制 Employee 的實現類的創(chuàng)建,并通過各種途徑注入到 Company 中。依賴注入的類圖如下所示。
注:在某些文章或框架中,注入器也被命名為容器(Containter或 IoC Container)。
依賴注入的注入方式有以下幾種:
- 構造器注入(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 的實現類時,即可返回一個特定類型的實現類。服務定位的類圖如下所示。
從類圖中可以看出,服務定位與依賴注入的區(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)服務定位兩種實現方式。依賴注入和服務定位雖然有些差異,但是兩者的目標是一致的,就是為了實現控制反轉。
整體上而言,控制反轉雖然能夠對代碼進行解耦,但是凡事都有兩面性。它會讓代碼邏輯變得更加復雜,調試也會更加困難。從這方面看來,關于是否采用控制反轉,需要具體場景具體分析,權衡利弊之后再作出選擇。