概述
依賴(lài)注入(Denpendecy Injection ,DI) 通常和 控制反轉(zhuǎn)(Inverse of Control,IoC) 一起出現(xiàn).它是實(shí)現(xiàn)IoC的主要手段之一.通過(guò)依賴(lài)注入類(lèi)可以不關(guān)心自身的依賴(lài)應(yīng)該如何構(gòu)造,而是由注入器代理這個(gè)職責(zé),將類(lèi)需要的依賴(lài)構(gòu)建好后注入到類(lèi)里.可以達(dá)到分離關(guān)注點(diǎn),分離調(diào)用方和依賴(lài),提高可復(fù)用性和可維護(hù)性.
為什么需要依賴(lài)注入
為什么需要依賴(lài)注入呢? 這和為什么需要IoC的原因基本相同.
在常規(guī)的開(kāi)發(fā)過(guò)程中,很多時(shí)候一個(gè)類(lèi)都是要依賴(lài)于其他的類(lèi)才能實(shí)現(xiàn)某些功能,才能夠更好的將關(guān)注點(diǎn)分離,比如一個(gè)車(chē) Car
類(lèi) 內(nèi)部需要使用燃油引擎 Engine
類(lèi),那么他需要在 Car
的內(nèi)部做類(lèi)似 new Engine()
這樣的操作, 看起來(lái)沒(méi)有任何的問(wèn)題,但是當(dāng)需要更換引擎為他的子類(lèi)電氣引擎ElectricEngine
的時(shí)候,問(wèn)題就來(lái)了,我們需要在 Car
的內(nèi)部修改 Engine
類(lèi),或者是當(dāng) Engine
做了修改需要額外的參數(shù)的時(shí)候,也會(huì)需要改動(dòng)到 Car
的內(nèi)部,并且每個(gè)類(lèi)都得對(duì)自身的依賴(lài)有更多的了解,這就違反了底米特原則(todo) 和 開(kāi)閉原則(todo)導(dǎo)致了代碼的耦合度極高,不利于維護(hù)和擴(kuò)展.
如圖
這個(gè)時(shí)候我們將這個(gè)依賴(lài)創(chuàng)建獲得的權(quán)利交給外部去維護(hù),那么這個(gè)類(lèi)只需要聲明他需要一個(gè) Engine
即可,至于是燃油還是電氣的引擎就由外部控制,此時(shí)依賴(lài)不再是內(nèi)部生成,而是通過(guò)外部注入得到,再碰到這樣的問(wèn)題的時(shí)候,就不需要更改 Car
內(nèi)部的代碼了.而是由外部代碼控制即可.
如圖
依賴(lài)注入尾為我們解決了以下問(wèn)題:
- 如何讓一個(gè)類(lèi)和如何創(chuàng)建這個(gè)類(lèi)互不關(guān)聯(lián)
- 如何讓類(lèi)支持一定程度的可變性,提高可復(fù)用度
- 如何通過(guò)一些外部配置來(lái)控制對(duì)象的創(chuàng)建
- 如何讓一個(gè)應(yīng)用支持不同的配置
細(xì)節(jié)
組成結(jié)構(gòu)
依賴(lài)注入將依賴(lài)的創(chuàng)建和依賴(lài)的行為隔離開(kāi),這使得程序變得更加松耦合并且更加符合依賴(lài)反轉(zhuǎn)原則(todo)和單一職責(zé)原則(todo).
最完全的依賴(lài)注入包含以下幾個(gè)部分
- 服務(wù). 要使用的對(duì)象.
- 客戶端. 依賴(lài)服務(wù)的對(duì)象.
- 接口. 定義客戶端如何使用服務(wù).
- 注入器. 構(gòu)建服務(wù)并注入到客戶端中.
UML圖如下:
用前面的汽車(chē)的例子對(duì)應(yīng)
- 服務(wù).
Engine
,ElectricEngine
- 客戶端.
Car
- 接口. 比如 Engine 實(shí)現(xiàn)了一個(gè)
IEngine
接口,Car
內(nèi)部使用IEngine
前面的例子簡(jiǎn)化了這一步,我們可以看到像Spring 中使用的時(shí)候有時(shí)候也是會(huì)存在簡(jiǎn)化的情況. - 注入器. 將
Engine
注入到Car
中的對(duì)象.
常見(jiàn)的簡(jiǎn)化是直接使用了父子類(lèi)來(lái)替代接口進(jìn)行操作,這個(gè)需要根據(jù)情況具體分析.當(dāng)然,這么做勢(shì)必會(huì)違反依賴(lài)倒置原則和影響解耦程度.所以最優(yōu)的情況是按照要求完整的實(shí)現(xiàn)它.
客戶端不應(yīng)該知道具體的依賴(lài)實(shí)現(xiàn),而是只關(guān)心接口,而接口后面的變化則不會(huì)對(duì)客戶端產(chǎn)生任何影響.
注入器通常也有很多別的名稱(chēng),比如 提供者,容器,工廠等
實(shí)現(xiàn)方式
依賴(lài)注入有如下實(shí)現(xiàn)方式:
- 手動(dòng)注入. 不利于擴(kuò)展,當(dāng)程序不斷龐大的時(shí)候,維護(hù)難度變高,并且需要維護(hù)大量的模板代碼.
- 基于接口.實(shí)現(xiàn)特定接口以供外部容器注入所依賴(lài)類(lèi)型的對(duì)象.優(yōu)點(diǎn)是依賴(lài)可以完全不了解客戶端,都對(duì)接口負(fù)責(zé)即可.
- 基于 set 方法.實(shí)現(xiàn)特定屬性的
set
方法,來(lái)讓外部容器注入所依賴(lài)類(lèi)型的對(duì)象.靈活性較高,任何時(shí)候都可以修改,但是由于過(guò)于靈活,無(wú)法確保依賴(lài)的完整,而且在所需依賴(lài)較多的時(shí)候,調(diào)用者難以判斷需要哪些依賴(lài).是否注入完整.并且暴露了注入接口,可能被其他對(duì)象意外調(diào)用. - 基于構(gòu)造函數(shù).實(shí)現(xiàn)特定參數(shù)的構(gòu)造函數(shù),在新建對(duì)象時(shí)注入所依賴(lài)類(lèi)型的對(duì)象. 使得依賴(lài)在剛創(chuàng)建對(duì)象的時(shí)候就被注入,保證了可靠性.但是靈活性較低,后面無(wú)法修改.
- 自動(dòng)注入. 便于維護(hù),開(kāi)發(fā)效率較高,代碼清晰
- 基于注解. 基于Java的注解功能(todo),在私有變量前加
@Autowired
等注解,不需要顯式的定義手動(dòng)注入的那些代碼,就可以讓外部容器注入對(duì)應(yīng)的對(duì)象.相當(dāng)于定義了public的set方法,但是因?yàn)闆](méi)有真正的set方法,減少了暴露的接口(使用set方法的話必須設(shè)置set方法為 public , 導(dǎo)致外部其他對(duì)象都可以顯式隨意的訪問(wèn)這個(gè)接口,但是這個(gè)依賴(lài)是否想要讓外部訪問(wèn)修改還是個(gè)未知數(shù).)
- 基于注解. 基于Java的注解功能(todo),在私有變量前加
一般情況下使用 自動(dòng)注入,注解注入的情況比較多.
目前比如 Spring,Guice,Dagger 等框架都實(shí)現(xiàn)了依賴(lài)注入的功能.
代碼示例
- 沒(méi)有依賴(lài)注入的情況
/** Client without DI */
public class Client {
// Internal reference to the service used by this client
private ServiceImpl service;
Client() {
// Specify a specific implementation in the constructor instead of using dependency injection
service = new ServiceImpl();
}
public void doSth() {
service.doSth();
}
}
- 基于接口
/** Client with interface DI */
public class Client implements ServiceSetter {
// Internal reference to the service used by this client
private Service service;
Client() {}
public void doSth() {
service.doSth();
}
@Override
public void setService(Service service) {
this.service = service;
}
}
/** Service setter interface. */
public interface ServiceSetter {
void setService(Service service);
}
?
- 基于 set 方法
/** Client with setter DI */
public class Client {
// Internal reference to the service used by this client
private Service service;
Client() {}
public void doSth() {
service.doSth();
}
/**
* set dependency in .
*/
public void setService(Service service) {
this.service = service;
}
}
- 基于構(gòu)造器
/** Client with constructor DI */
public class Client {
// Internal reference to the service used by this client
private Service service;
// set dependency with constructor
Client(Service service) {
this.service = service;
}
public void doSth() {
service.doSth();
}
}
- 基于注解
/** Client with annotation DI */
public class Client {
// Internal reference to the service used by this client
@Inject
private Service service;
Client() {}
public void doSth() {
service.doSth();
}
}
優(yōu)點(diǎn)
- 提高了客戶端的靈活性,使得代碼的可配置性得到提高. 客戶端只關(guān)注自己需要的接口行為,而不強(qiáng)行綁定接口背后的實(shí)現(xiàn)邏輯,使得實(shí)現(xiàn)邏輯在一定程度上可以改變而不影響到客戶端.
- 使得應(yīng)用可以同時(shí)適配多種情況,通過(guò)注入不同的對(duì)象實(shí)現(xiàn)不同的邏輯.程序靈活性大大提高,在測(cè)試等方面有顯著的效果.
- 分離關(guān)注點(diǎn).客戶端不再關(guān)心依賴(lài)的創(chuàng)建和許多細(xì)節(jié)問(wèn)題.這樣更有利于單元測(cè)試等.
- 提高了代碼的復(fù)用性,可測(cè)試性和可維護(hù)性.
- 減少了模板代碼,將創(chuàng)建依賴(lài)都交給了注入器去管理.
- 更有利于協(xié)同開(kāi)發(fā).由于大家都只對(duì)接口負(fù)責(zé),那么不同的模塊也可以方便有效的交給不同的人開(kāi)發(fā).類(lèi)似插件的開(kāi)發(fā)方式
- 減少了客戶端和依賴(lài)間的耦合
- 分離了類(lèi)的使用和創(chuàng)建
缺點(diǎn)
如果沒(méi)有使用合適的方法,會(huì)導(dǎo)致一些存在比較固定的默認(rèn)對(duì)象的創(chuàng)建過(guò)程重復(fù)太多次,不利于維護(hù).
提高了代碼的學(xué)習(xí)成本,開(kāi)發(fā)者必須了解DI的原理并且額外找到注入的對(duì)象才能理解這部分的邏輯.
提高了代碼的使用難度.由于DI通常是通過(guò)反射或者APT技術(shù)實(shí)現(xiàn)的,所以對(duì)于IDE中的一些自動(dòng)化操作會(huì)有所影響,比如 找到引用類(lèi),調(diào)用層級(jí)之類(lèi)的.
提高了前期開(kāi)發(fā)的成本.由于引入了DI技術(shù),導(dǎo)致前期初始化依賴(lài)要做的事情變得復(fù)雜.雖然后期的維護(hù)成本將大大下降.
About Me
我的博客 leonchen1024.com
我的 GitHub https://github.com/LeonChen1024
微信公眾號(hào)