什么是控制反轉(zhuǎn)?什么是依賴注入?這些類型的問題通常會遇到代碼示例,模糊解釋以及StackOverflow上標識為“ 低質(zhì)量答案 ”的問題。
我們使用控制反轉(zhuǎn)和依賴注入,并經(jīng)常將其作為構(gòu)建應(yīng)用程序的正確方法。然而,我們無法清晰地闡明原因!
原因是我們還沒有清楚地確定控制是什么。一旦我們理解了我們正在反轉(zhuǎn)的內(nèi)容,控制反轉(zhuǎn)與依賴注入的概念實際上并不是要問的問題。它實際上變成了以下內(nèi)容:
控制反轉(zhuǎn) = 依賴(狀態(tài))注入 + 線程注入 + 連續(xù)(函數(shù))注入
為了解釋這一點,我們來寫一些代碼。是的,使用代碼來解釋控制反轉(zhuǎn)的明顯問題正在重復,但請耐心等待,答案一直在你眼前。
一個明確使用控制反轉(zhuǎn)/依賴注入的模式是存儲庫模式,來避免繞過連接。而不是以下:
public class NoDependencyInjectionRepository implements Repository<Entity> {
public void save(Entity entity, Connection connection) throws SQLException {
// Use connection to save entity to database
}
}
依賴注入允許將存儲庫重新實現(xiàn)為:
public class DependencyInjectionRepository implements Repository<Entity> {
@Inject Connection connection;
public void save(Entity entity) throws SQLException {
// Use injected connection to save entity to database
}
}
現(xiàn)在,你看到我們剛剛解決的問題了嗎?
如果您正在考慮“我現(xiàn)在可以更改 connection 來使用REST調(diào)用” ,這一切都可以靈活改變,那么您就會很接近這個問題。
要查看問題是否已解決,請不要查看實現(xiàn)類。相反,看看接口。客戶端調(diào)用代碼已經(jīng)從:
repository.save(entity, connection);
變?yōu)橐韵聝?nèi)容:
repository.save(entity);
我們已經(jīng)移除了客戶端代碼的耦合,以提供一個 connection 在調(diào)用方法上。通過刪除耦合,我們可以替換存儲庫的不同實現(xiàn)(再次,無聊的代碼,但請忍受我):
public class WebServiceRepository implements Repository<Entity> {
@Inject WebClient client;
public void save(Entity entity) {
// Use injected web client to save entity
}
}
客戶端能夠繼續(xù)調(diào)用方法:
repository.save(entity);
客戶端不知道存儲庫現(xiàn)在調(diào)用微服務(wù)來保存實體而不是直接與數(shù)據(jù)庫通信。(實際上,客戶已經(jīng)知道,但我們很快就會談到這一點。)
因此,將此問題提升到關(guān)于該方法的抽象級別:
R method(P1 p1, P2 p2) throws E1, E2
// with dependency injection becomes
@Inject P1 p1;
@Inject P2 p2;
R method() throws E1, E2
通過依賴注入消除了客戶端為該方法提供參數(shù)的耦合。
現(xiàn)在,你看到耦合的其他四個問題了嗎?
在這一點上,我警告你,一旦我向你展示耦合問題,你將永遠不會再看同樣的代碼了。 這是矩陣中我要問你是否想要紅色或藍色的要點。一旦我向你展示這個問題真正的兔子洞有多遠,就沒有回頭了 - 實際上沒有必要進行重構(gòu),而且在建模邏輯和計算機科學的基礎(chǔ)知識方面存在問題(好的,大的聲明,但請繼續(xù)閱讀 - 我不會把它放在任何其他方式)。
所以,你選擇了紅點。
讓我們?yōu)槟阕龊脺蕚洹?/p>
為了識別四個額外的耦合問題,讓我們再看一下抽象方法:
@Inject P1 p1;
@Inject P2 p2;
R method() throws E1, E2
// and invoking it
try {
R result = object.method();
} catch (E1 | E2 ex) {
// handle exception
}
什么是客戶端代碼耦合?
返回類型
方法名稱
處理異常
提供給該方法的線程
依賴注入允許我更改方法所需的對象,而無需更改調(diào)用方法的客戶端代碼。但是,如果我想通過以下方式更改我的實現(xiàn)方法:
更改其返回類型
修改它的名稱
拋出一個新的異常(在上面的交換到微服務(wù)存儲庫的情況下,拋出HTTP異常而不是SQL異常)
使用不同的線程(池)執(zhí)行方法而不是客戶端調(diào)用提供的線程
這涉及“ 重構(gòu) ”我的方法的所有客戶端代碼。當實現(xiàn)具有實際執(zhí)行功能的艱巨任務(wù)時,為什么調(diào)用者要求耦合?我們實際上應(yīng)該反轉(zhuǎn)耦合,以便實現(xiàn)可以指示方法簽名(而不是調(diào)用者)。
你可能就像Neo在黑客帝國中所做的那樣“哼”一下嗎?讓實現(xiàn)定義他們的方法簽名?但是,不是覆蓋和實現(xiàn)抽象方法簽名定義的整個OO原則嗎?這樣只會導致更混亂,因為如果它的返回類型,名稱,異常,參數(shù)隨著實現(xiàn)的發(fā)展而不斷變化,我如何調(diào)用該方法?
簡單。你已經(jīng)知道了模式。你只是沒有看到他們一起使用,他們的總和比他們的部分更強大。
因此,讓我們遍歷方法的五個耦合點(返回類型,方法名稱,參數(shù),異常,調(diào)用線程)并將它們分離。
我們已經(jīng)看到依賴注入刪除了客戶端的參數(shù)耦合,所以一個個向下。
接下來,讓我們處理方法名稱。
方法名稱解耦
許多語言(包括Java lambdas)允許或具有該語言的一等公民的功能。通過創(chuàng)建對方法的函數(shù)引用,我們不再需要知道方法名稱來調(diào)用該方法:
Runnable f1 = () -> object.method();
// Client call now decoupled from method name
f1.run()
我們現(xiàn)在甚至可以通過依賴注入傳遞方法的不同實現(xiàn):
@Inject Runnable f1;
void clientCode() {
f1.run(); // to invoke the injected method
}
好的,這是一些額外的代碼,沒有太大的額外價值。但是,再次,忍受我。我們已將方法的名稱與調(diào)用者分離。
接下來,讓我們解決方法中的異常。
方法異常解耦
通過使用上面的注入函數(shù)技術(shù),我們注入函數(shù)來處理異常:
Runnable f1 = () -> {
@Inject Consumer<E1> h1;
@Inject Consumer<E2> h2;
try {
object.method();
} catch (E1 e1) {
h1.accept(e1);
} catch (E2 e2) {
h2.accept(e2);
}
}
// 注意:上面是用于標識概念的抽象偽代碼(我們將很快編譯代碼)
現(xiàn)在,異常不再是客戶端調(diào)用者的問題。注入的方法現(xiàn)在處理將調(diào)用者與必須處理異常分離的異常。
接下來,讓我們處理調(diào)用線程。
方法的調(diào)用線程解耦
通過使用異步函數(shù)簽名并注入Executor,我們可以將調(diào)用實現(xiàn)方法的線程與調(diào)用者提供的線程分離:
Runnable f1 = () -> {
@Inject Executor executor;
executor.execute(() -> {
object.method();
});
}
通過注入適當?shù)?Executor,我們可以使用我們需要的任何線程池調(diào)用的實現(xiàn)方法。要重用客戶端的調(diào)用線程,我們只需要同步Exectutor:
Executor synchronous = (runnable) -> runnable.run();
所以現(xiàn)在,我們可以解耦一個線程,從調(diào)用代碼的線程執(zhí)行實現(xiàn)方法。
但是沒有返回值,我們?nèi)绾卧诜椒ㄖg傳遞狀態(tài)(對象)?讓我們將它們與依賴注入結(jié)合在一起。
控制(耦合)反轉(zhuǎn)
讓我們將上述模式與依賴注入相結(jié)合,得到ManagedFunction:
public interface ManagedFunction {
void run();
}
public class ManagedFunctionImpl implements ManagedFunction {
@Inject P1 p1;
@Inject P2 p2;
@Inject ManagedFunction f1; // other method implementations to invoke
@Inject ManagedFunction f2;
@Inject Consumer<E1> h1;
@Inject Consumer<E2> h2;
@Inject Executor executor;
@Override
public void run() {
executor.execute(() -> {
try {
implementation(p1, p2, f1, f2);
} catch (E1 e1) {
h1.accept(e1);
} catch (E2 e2) {
h2.accept(e2);
});
}
private void implementation(
P1 p1, P2 p2,
ManagedFunction f1, ManagedFunction f2
) throws E1, E2 {
// use dependency inject objects p1, p2
// invoke other methods via f1, f2
// allow throwing exceptions E1, E2
}
}
好的,這里有很多東西,但它只是上面的模式結(jié)合在一起。客戶端代碼現(xiàn)在完全與方法實現(xiàn)分離,因為它只運行:
@Inject ManagedFunction function;
public void clientCode() {
function.run();
}
現(xiàn)在可以自由更改實現(xiàn)方法,而不會影響客戶端調(diào)用代碼:
方法沒有返回類型(一般的限制可以使用void,但是異步代碼是必需的)
實現(xiàn)方法名稱可能會更改,因為它包含在 ManagedFunction.run()
不再需要參數(shù)ManagedFunction。這些是依賴注入的,允許實現(xiàn)方法選擇它需要哪些參數(shù)(對象)
異常由注入的Consumers處理。實現(xiàn)方法現(xiàn)在可以規(guī)定它拋出的異常,只需要Consumers 注入不同的異常 。客戶端調(diào)用代碼不需要知道實現(xiàn)方法,現(xiàn)在可以自定義拋出 HTTPException 而不是 SQLException 。此外, Consumers 實際上可以通過ManagedFunctions 注入異常來實現(xiàn) 。
注入Executor 允許實現(xiàn)方法通過指定注入的Executor來指示其執(zhí)行的線程 。這可能導致重用客戶端的調(diào)用線程或讓實現(xiàn)由單獨的線程或線程池運行
現(xiàn)在,通過其調(diào)用者的方法的所有五個耦合點都是分離的。
我們實際上已經(jīng)“對耦合進行了反向控制”。換句話說,客戶端調(diào)用者不再指定實現(xiàn)方法可以命名的內(nèi)容,用作參數(shù),拋出異常,使用哪個線程等。耦合的控制被反轉(zhuǎn),以便實現(xiàn)方法可以決定它耦合到什么指定它是必需的注射。
此外,由于調(diào)用者沒有耦合,因此不需要重構(gòu)代碼。實現(xiàn)發(fā)生變化,然后將其耦合(注入)配置到系統(tǒng)的其余部分。客戶端調(diào)用代碼不再需要重構(gòu)。
因此,實際上,依賴注入只解決了方法耦合問題的1/5。對于僅解決20%問題非常成功的事情,它確實顯示了該方法的耦合問題究竟有多少。
實現(xiàn)上述模式將創(chuàng)建比您的系統(tǒng)中更多的代碼。這就是為什么開源框架OfficeFloor是控制框架的“真正”反轉(zhuǎn),并且已經(jīng)整合在一起以減輕此代碼的負擔。這是上述概念中的一個實驗,以查看真實系統(tǒng)是否更容易構(gòu)建和維護,具有“真正的”控制反轉(zhuǎn)。
摘要
因此,下次你遇到Refactor Button / Command時,意識到這是通過每次編寫代碼時一直盯著我們的方法的耦合引起的。
真的,為什么我們有方法簽名?這是因為線程堆棧。我們需要將內(nèi)存加載到線程堆棧中,并且方法簽名遵循計算機的行為。但是,在現(xiàn)實世界中,對象之間行為的建模不提供線程堆棧。對象都是通過很小的接觸點松耦合 - 而不是由該方法施加的五個耦合方面。
此外,在計算中,我們努力實現(xiàn)低耦合和高內(nèi)聚。有人可能會提出一個案例,來對比ManagedFunctions,方法是:
高耦合:方法有五個方面耦合到客戶端調(diào)用代碼
低內(nèi)聚:隨著方法處理異常和返回類型開始模糊方法的責任隨著時間的推移,持續(xù)變化和快捷方式會迅速降低方法實施的凝聚力,開始處理超出其責任的邏輯
由于我們力求低耦合和高內(nèi)聚,我們最基本的構(gòu)建塊( method 和 function)可能實際上違背了我們最核心的編程原則。