Java中的控制(耦合)反轉

什麼是控制反轉?什麼是依賴注入?這些類型的問題通常會遇到代碼示例,模糊解釋以及StackOverflow上標識為“ 低質量答案 ”的問題。

我們使用控制反轉和依賴注入,並經常將其作為構建應用程序的正確方法。然而,我們無法清晰地闡明原因!

原因是我們還沒有清楚地確定控制是什麼。一旦我們理解了我們正在反轉的內容,控制反轉與依賴注入的概念實際上並不是要問的問題。它實際上變成了以下內容:

控制反轉 = 依賴(狀態)注入 + 線程注入 + 連續(函數)注入

為了解釋這一點,我們來寫一些代碼。是的,使用代碼來解釋控制反轉的明顯問題正在重複,但請耐心等待,答案一直在你眼前。

一個明確使用控制反轉/依賴注入的模式是存儲庫模式,來避免繞過連接。而不是以下:

public

class

NoDependencyInjectionRepository

implements

Repository
<
Entity
>

{


public

void
save
(
Entity
entity
,

Connection
connection
)

throws

SQLException

{

// Use connection to save entity to database

}
}

依賴注入允許將存儲庫重新實現為:

public

class

DependencyInjectionRepository

implements

Repository
<
Entity
>

{

@Inject

Connection
connection
;

public

void
save
(
Entity
entity
)

throws

SQLException

{

// Use injected connection to save entity to database

}
}

現在,你看到我們剛剛解決的問題了嗎?

如果您正在考慮“我現在可以更改 connection 來使用REST調用” ,這一切都可以靈活改變,那麼您就會很接近這個問題。

要查看問題是否已解決,請不要查看實現類。相反,看看接口。客戶端調用代碼已經從:

repository
.
save
(
entity
,
connection
);

變為以下內容:

repository
.
save
(
entity
);

我們已經移除了客戶端代碼的耦合,以提供一個 connection 在調用方法上。通過刪除耦合,我們可以替換存儲庫的不同實現(再次,無聊的代碼,但請忍受我):

public

class

WebServiceRepository

implements

Repository
<
Entity
>

{

@Inject

WebClient
client
;

public

void
save
(
Entity
entity
)

{

// Use injected web client to save entity

}
}

客戶端能夠繼續調用方法:

repository
.
save
(
entity
);

客戶端不知道存儲庫現在調用微服務來保存實體而不是直接與數據庫通信。(實際上,客戶已經知道,但我們很快就會談到這一點。)

因此,將此問題提升到關於該方法的抽象級別:

R method
(
P1 p1
,
P2 p2
)

throws
E1
,
E2
// with dependency injection becomes
@Inject
P1 p1
;
@Inject
P2 p2
;
R method
()

throws
E1

,
E2

通過依賴注入消除了客戶端為該方法提供參數的耦合。

現在,你看到耦合的其他四個問題了嗎?

在這一點上,我警告你,一旦我向你展示耦合問題,你將永遠不會再看同樣的代碼了。 這是矩陣中我要問你是否想要紅色或藍色的要點。一旦我向你展示這個問題真正的兔子洞有多遠,就沒有回頭了 - 實際上沒有必要進行重構,而且在建模邏輯和計算機科學的基礎知識方面存在問題(好的,大的聲明,但請繼續閱讀 - 我不會把它放在任何其他方式)。

所以,你選擇了紅點。

讓我們為你做好準備。

為了識別四個額外的耦合問題,讓我們再看一下抽象方法:

@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
}

什麼是客戶端代碼耦合?

  • 返回類型
  • 方法名稱
  • 處理異常
  • 提供給該方法的線程

依賴注入允許我更改方法所需的對象,而無需更改調用方法的客戶端代碼。但是,如果我想通過以下方式更改我的實現方法:

  • 更改其返回類型
  • 修改它的名稱
  • 拋出一個新的異常(在上面的交換到微服務存儲庫的情況下,拋出HTTP異常而不是SQL異常)
  • 使用不同的線程(池)執行方法而不是客戶端調用提供的線程

這涉及“ 重構 ”我的方法的所有客戶端代碼。當實現具有實際執行功能的艱鉅任務時,為什麼調用者要求耦合?我們實際上應該反轉耦合,以便實現可以指示方法簽名(而不是調用者)。

你可能就像Neo在黑客帝國中所做的那樣“哼”一下嗎?讓實現定義他們的方法簽名?但是,不是覆蓋和實現抽象方法簽名定義的整個OO原則嗎?這樣只會導致更混亂,因為如果它的返回類型,名稱,異常,參數隨著實現的發展而不斷變化,我如何調用該方法?

簡單。你已經知道了模式。你只是沒有看到他們一起使用,他們的總和比他們的部分更強大。

因此,讓我們遍歷方法的五個耦合點(返回類型,方法名稱,參數,異常,調用線程)並將它們分離。

我們已經看到依賴注入刪除了客戶端的參數耦合,所以一個個向下。

接下來,讓我們處理方法名稱。

方法名稱解耦

許多語言(包括Java lambdas)允許或具有該語言的一等公民的功能。通過創建對方法的函數引用,我們不再需要知道方法名稱來調用該方法:

Runnable
f1
=

()

->
object
.
method
();
// Client call now decoupled from method name
f1
.
run
()

我們現在甚至可以通過依賴注入傳遞方法的不同實現:

@Inject

Runnable
f1
;
void
clientCode
()


{
f1
.
run
();

// to invoke the injected method
}

好的,這是一些額外的代碼,沒有太大的額外價值。但是,再次,忍受我。我們已將方法的名稱與調用者分離。

接下來,讓我們解決方法中的異常。

方法異常解耦

通過使用上面的注入函數技術,我們注入函數來處理異常:

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
);

}
}
// 注意:上面是用於標識概念的抽象偽代碼(我們將很快編譯代碼)

現在,異常不再是客戶端調用者的問題。注入的方法現在處理將調用者與必須處理異常分離的異常。

接下來,讓我們處理調用線程。

方法的調用線程解耦

通過使用異步函數簽名並注入Executor,我們可以將調用實現方法的線程與調用者提供的線程分離:

Runnable
f1
=

()

->

{

@Inject

Executor
executor
;
executor
.
execute
(()

->

{
object
.
method
();

});
}

通過注入適當的 Executor,我們可以使用我們需要的任何線程池調用的實現方法。要重用客戶端的調用線程,我們只需要同步Exectutor:

Executor
synchronous
=

(
runnable
)

->
runnable
.
run
();

所以現在,我們可以解耦一個線程,從調用代碼的線程執行實現方法。

但是沒有返回值,我們如何在方法之間傳遞狀態(對象)?讓我們將它們與依賴注入結合在一起。

控制(耦合)反轉

讓我們將上述模式與依賴注入相結合,得到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

}
}

好的,這裡有很多東西,但它只是上面的模式結合在一起。客戶端代碼現在完全與方法實現分離,因為它只運行:

@Inject

ManagedFunction
function
;
public

void
clientCode
()

{
function
.
run
();
}

現在可以自由更改實現方法,而不會影響客戶端調用代碼:

  • 方法沒有返回類型(一般的限制可以使用void,但是異步代碼是必需的)
  • 實現方法名稱可能會更改,因為它包含在 ManagedFunction.run()
  • 不再需要參數ManagedFunction。這些是依賴注入的,允許實現方法選擇它需要哪些參數(對象)
  • 異常由注入的Consumers處理。實現方法現在可以規定它拋出的異常,只需要Consumers 注入不同的異常 。客戶端調用代碼不需要知道實現方法,現在可以自定義拋出 HTTPException 而不是 SQLException 。此外, Consumers 實際上可以通過ManagedFunctions 注入異常來實現 。
  • 注入Executor 允許實現方法通過指定注入的Executor來指示其執行的線程 。這可能導致重用客戶端的調用線程或讓實現由單獨的線程或線程池運行

現在,通過其調用者的方法的所有五個耦合點都是分離的。

我們實際上已經“對耦合進行了反向控制”。換句話說,客戶端調用者不再指定實現方法可以命名的內容,用作參數,拋出異常,使用哪個線程等。耦合的控制被反轉,以便實現方法可以決定它耦合到什麼指定它是必需的注射。

此外,由於調用者沒有耦合,因此不需要重構代碼。實現發生變化,然後將其耦合(注入)配置到系統的其餘部分。客戶端調用代碼不再需要重構。

因此,實際上,依賴注入只解決了方法耦合問題的1/5。對於僅解決20%問題非常成功的事情,它確實顯示了該方法的耦合問題究竟有多少。

實現上述模式將創建比您的系統中更多的代碼。這就是為什麼開源框架OfficeFloor是控制框架的“真正”反轉,並且已經整合在一起以減輕此代碼的負擔。這是上述概念中的一個實驗,以查看真實系統是否更容易構建和維護,具有“真正的”控制反轉。

摘要

因此,下次你遇到Refactor Button / Command時,意識到這是通過每次編寫代碼時一直盯著我們的方法的耦合引起的。

真的,為什麼我們有方法簽名?這是因為線程堆棧。我們需要將內存加載到線程堆棧中,並且方法簽名遵循計算機的行為。但是,在現實世界中,對象之間行為的建模不提供線程堆棧。對象都是通過很小的接觸點松耦合 - 而不是由該方法施加的五個耦合方面。

此外,在計算中,我們努力實現低耦合和高內聚。有人可能會提出一個案例,來對比ManagedFunctions,方法是:

  • 高耦合:方法有五個方面耦合到客戶端調用代碼
  • 低內聚:隨著方法處理異常和返回類型開始模糊方法的責任隨著時間的推移,持續變化和快捷方式會迅速降低方法實施的凝聚力,開始處理超出其責任的邏輯

由於我們力求低耦合和高內聚,我們最基本的構建塊( method 和 function)可能實際上違背了我們最核心的編程原則。


分享到:


相關文章: