依賴注入 (DI) 是一種讓類從外部接收其依賴的技術(shù)。如果類 A 使用類 B,則類 A 依賴于類 B,而 B 是 A 的依賴項(xiàng)。
以下示例顯示了 Java 中的依賴項(xiàng)和 DI 是什么。在第一個(gè)示例中,A 類依賴于 B 類,因?yàn)?B 是 A 的成員。A 和 B 是緊密耦合的。每當(dāng) B 改變時(shí),A 就必須改變。這種情況稱為硬依賴。
// hard dependency
class A{
private B b;
public A(){
this.b = new B();
}
...
}
在第二個(gè)例子中,A 仍然依賴于 B,但依賴不是硬編碼的。它通過在構(gòu)造函數(shù)中使用參數(shù)來解耦。如果 A 需要 B 的不同實(shí)現(xiàn),A 可以使用 B 的不同實(shí)現(xiàn)來構(gòu)造實(shí)例。這導(dǎo)致了 DI 的一個(gè)關(guān)鍵特性:被注入的類應(yīng)該是一個(gè)抽象接口,以便可以將不同的實(shí)現(xiàn)注入到 A。如果 B 的實(shí)現(xiàn)只有一個(gè),則不需要進(jìn)行 DI。
// dependency injection through constructor
class A{
private B b;
public A(B b){
this.b = b;
}
...
}
使用依賴注入的好處
DI 的一個(gè)示例用途是數(shù)據(jù)訪問對(duì)象 (DAO)。執(zhí)行 CRUD 操作的類通常需要訪問數(shù)據(jù)庫。使用 DI 向應(yīng)用程序注入 DAO 將應(yīng)用程序?qū)优c數(shù)據(jù)持久層解耦。如果底層數(shù)據(jù)庫發(fā)生變化,只要這些 DAO 實(shí)現(xiàn)相同的接口,應(yīng)用程序類就可以更改為不同的 DAO。另一個(gè)好處是使單元測試更容易。單元測試可以使用偽造的(硬編碼或內(nèi)存中的)DAO 來測試應(yīng)用程序邏輯,而無需擔(dān)心底層數(shù)據(jù)庫訪問。
DI 是流行的 Java 框架(如 Spring 和 Hibernate)中使用的一項(xiàng)關(guān)鍵技術(shù)??蚣懿皇鞘謩?dòng)創(chuàng)建 B 對(duì)象并將其傳遞給 A 的構(gòu)造函數(shù),而是使用反射來創(chuàng)建依賴對(duì)象并根據(jù)配置將它們注入到適當(dāng)?shù)奈恢谩?/p>
一個(gè)展示依賴注入的簡單例子
下面是一個(gè)簡單的例子來說明使用框架時(shí) DI 的樣子以及使用 DI 的兩個(gè)好處。我使用 Guice 框架,但其他框架在幕后以相同的方式工作。
假設(shè)我們有一臺(tái)計(jì)算機(jī),它有很多部分協(xié)同工作,例如 CPU、內(nèi)存等。 CPU 中有兩種方法。
public interface CPU {
public void start();
public int getUsage();
}
CPU 可以是 Intel,
public class Intel implements CPU{
@Override
public void start() {
System.out.println("Intel is started.");
}
@Override
public int getUsage() {
return new Random().nextInt(100);
}
}
或 AMD。
public class Amd implements CPU {
@Override
public void start() {
System.out.print("Amd is started");
}
@Override
public int getUsage() {
return new Random().nextInt(100);
}
}
在 Guice 中,通過構(gòu)造函數(shù)注入依賴就像添加 @Inject 注釋一樣簡單。
public class Computer {
private CPU cpu;
@Inject
Computer(CPU cpu) {
this.cpu = cpu;
}
public void start() {
cpu.start();
// start other parts
}
public boolean isStatusOk() {
//assuming this random
if (cpu.getUsage() > 50) {
return false;
}
// check other things, such as memory, hard drives.
return true;
}
public static void main(String[] args) {
Injector injector = Guice.createInjector(new BasicModule());
Computer computer = injector.getInstance(Computer.class);
computer.start();
System.out.println("Status:" + (computer.isStatusOk() ? "OK" : "Not OK"));
}
}
Guice 使用模塊來配置注入。在此示例中,當(dāng)請(qǐng)求 CPU 時(shí),模塊將具體的 Intel 綁定到 CPU。
public class BasicModule extends AbstractModule {
@Override
protected void configure() {
bind(CPU.class).to(Intel.class);
}
}
這樣做的好處是顯而易見的。計(jì)算機(jī)可以在需要時(shí)靈活地使用其他類型的 CPU。此外,如果 CPU 依賴于另一個(gè)類,例如 Cache 或 Clock,我們可以使用相同的方式注入依賴項(xiàng),而無需耦合這些類。
關(guān)于第二個(gè)好處——讓單元測試更簡單,我們可以做一個(gè)簡單的單元測試來測試 isStatusOk() 方法。在實(shí)際情況下,CPU 使用率可以是基于實(shí)際使用情況的隨機(jī)數(shù)。如果我們想把測試的重點(diǎn)放在方法的其他部分,我們可以模擬 CPU 的使用情況,假設(shè) CPU 使用率沒問題,然后測試其他部分。
public class ComputerTest {
@Test
public void testIsStatusOk(){
CPU cpu = mock(CPU.class);
// mock cpu usage, so we can focus on testing other part
when(cpu.getUsage()).thenReturn(10);
assertTrue(new Computer(cpu).isStatusOk());
}
}
總之,DI 是為了分離對(duì)象創(chuàng)建和使用的關(guān)注點(diǎn)。DI 解耦類依賴并使單元測試更容易。