Java使用IOC控制反轉(zhuǎn)的三種設(shè)計(jì)模式詳解
對(duì)于許多開(kāi)發(fā)人員來(lái)說(shuō),控制反演(IoC)都是一個(gè)模糊的概念,因?yàn)樗麄冊(cè)诂F(xiàn)實(shí)世界中很少或沒(méi)有被應(yīng)用過(guò)。在最好的情況下,控制反演(IoC)可以加單的認(rèn)為是等效于依賴(lài)注入(DI)。實(shí)際上,只有在翻轉(zhuǎn)控制與依賴(lài)注入雙方都只是反映翻轉(zhuǎn)依賴(lài)管理控制的時(shí)候,才認(rèn)為兩者是等效的。雖然,依賴(lài)注入實(shí)際上是IoC的一種眾所周知的形式。但是,事實(shí)上IoC卻是一個(gè)相對(duì)更為廣泛的軟件設(shè)計(jì)范例,可以通過(guò)多種模式來(lái)進(jìn)行實(shí)現(xiàn)。在本文中,我們將介紹依賴(lài)注入,觀察者模式和模板方法模式如何實(shí)現(xiàn)控制反轉(zhuǎn)的。
正如許多其他設(shè)計(jì)模式,是從各種各樣的使用場(chǎng)景中總結(jié)出來(lái)的,IoC的實(shí)現(xiàn)方式,也是類(lèi)似的一種適合開(kāi)發(fā)者使用的折中方式:
一方面,高度解耦組件的設(shè)計(jì),以及將應(yīng)用邏輯封裝在一個(gè)單一的地方,是實(shí)現(xiàn)IoC的直接而又自然的一種方式。
另一方面,上述實(shí)現(xiàn)需要至少需要構(gòu)建一個(gè)間接層,然而在某些用例中,這可能又是一種過(guò)度設(shè)計(jì)了。
接下來(lái),不妨看幾個(gè)具體的實(shí)現(xiàn),這將有助于您了解,如何在這些屬性之間進(jìn)行權(quán)衡折中。
IOC范式揭秘
控制反轉(zhuǎn)是一種帶有某些特征的模式。下面,給出了由Martin Fowler給出的一個(gè)IOC經(jīng)典范例,該范例實(shí)現(xiàn)的功能是從控制臺(tái)中收集用戶(hù)數(shù)據(jù)。
public static void main(String[] args) {
while (true) {
BufferedReader userInputReader = new BufferedReader(
new InputStreamReader(System.in));
System.out.println("Please enter some text: ");
try {
System.out.println(userInputReader.readLine());
} catch (IOException e) {
e.printStackTrace();
}
}
}
這個(gè)用例中,在main方法中進(jìn)行流程控制:在無(wú)限循環(huán)調(diào)用中,讀取用戶(hù)輸入,并將讀取的內(nèi)容輸出到控制臺(tái)上。完全又main方法控制何時(shí)去讀取用戶(hù)輸入,何時(shí)去輸出。
考慮下,上述程序的一個(gè)新版本,該版本中需要通過(guò)圖形界面中的文本框來(lái)收件用戶(hù)輸入,另外還有個(gè)按鈕,該按鈕上綁定有一個(gè)action監(jiān)聽(tīng)器。這樣的話(huà),用戶(hù)每次點(diǎn)擊按鈕,輸入的文本由監(jiān)聽(tīng)器收集并打印到面板。
在這個(gè)版本的程序中,它實(shí)際上是由事件監(jiān)聽(tīng)器模型(在這種情況下,這是框架)的控制下,調(diào)用開(kāi)發(fā)者編寫(xiě)的用于讀取和打印用戶(hù)輸入的代碼。簡(jiǎn)單地說(shuō),框架將調(diào)用開(kāi)發(fā)人員的代碼,而不是其他方式。該框架實(shí)際上是一個(gè)可擴(kuò)展的結(jié)構(gòu),它為開(kāi)發(fā)人員提供了一組注入自定義代碼段的切入點(diǎn)。
這種情況下,控制已經(jīng)被有效的反轉(zhuǎn)了。
從更通用的角度來(lái)看,由框架定義的每個(gè)可調(diào)用擴(kuò)展點(diǎn)(以接口實(shí)現(xiàn),實(shí)現(xiàn)繼承(也稱(chēng)為子類(lèi))的形式)是IoC的一種明確定義的形式。
看下,下述這個(gè)簡(jiǎn)單的Servlet例子:
public class MyServlet extends HttpServlet {
protected void doPost(
HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
// developer implementation here
}
protected void doGet(
HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
// developer implementation here
}
}
此處,HttpServlet類(lèi)(屬于框架)是完全控制程序的元素,而不是MyServlet這個(gè)子類(lèi)。在由servlet容器創(chuàng)建之后,當(dāng)收到servlet的GET和POST的HTTP請(qǐng)求,doGet()和doPost()方法中的代碼會(huì)分別自動(dòng)調(diào)用。
與典型的繼承方式相比,即子類(lèi)是控制的元素,而不是基類(lèi),該例中,控件已經(jīng)被反轉(zhuǎn)了。
事實(shí)上,servlet的方法是模板方法模式的實(shí)現(xiàn),稍后我們?cè)偕钊胗懻摗?/p>
使用那些通過(guò)提供可擴(kuò)展API,秉承開(kāi)閉原則的框架時(shí),使用框架的開(kāi)發(fā)人員的角色,最終被歸結(jié)為定義自己的一組自定義類(lèi),即開(kāi)發(fā)人員要么通過(guò)實(shí)現(xiàn)框架提供的一個(gè)或多個(gè)接口方式,要么通過(guò)繼承現(xiàn)有基類(lèi)的方式。反過(guò)來(lái),類(lèi)的實(shí)例卻是直接框架進(jìn)行實(shí)例化,并且這些事例是被框架調(diào)用的。
此處引用Fowler的話(huà):該框架調(diào)用開(kāi)發(fā)人員,而不是開(kāi)發(fā)人員調(diào)用該框架。因此,IoC通常被稱(chēng)為好萊塢原則:不要打電話(huà)給我們,我們會(huì)打電話(huà)給你。
IOC的實(shí)現(xiàn)方式
該問(wèn)題上,顯而易見(jiàn)的是,實(shí)現(xiàn)控制反轉(zhuǎn)是有幾種不同方法的。我們不妨來(lái)總結(jié)一下,那些常見(jiàn)的實(shí)現(xiàn)方式。
注入依賴(lài)實(shí)現(xiàn)IOC
如前所述,注入依賴(lài)是IOC的一種實(shí)現(xiàn)方式,而且是最常見(jiàn)的一種面向?qū)ο笤O(shè)計(jì)方式。但是,思考一下:注入依賴(lài)究竟是如何達(dá)到控制反轉(zhuǎn)效果的呢?
為了回答這個(gè)問(wèn)題,我們給出如下一個(gè)原始的例子:
public interface UserQueue {
void add(User user);
void remove(User user);
User get();
}
public abstract class AbstractUserQueue implements UserQueue {
protected LinkedList<User> queue = new LinkedList<>();
@Override
public void add(User user) {
queue.addFirst(user);
}
@Override
public void remove(User user) {
queue.remove(user);
}
@Override
public abstract User get();
}
public class UserFifoQueue extends AbstractUserQueue {
public User get() {
return queue.getLast();
}
}
public class UserLifoQueue extends AbstractUserQueue {
public User get() {
return queue.getFirst();
}
}
UserQueue 接口定義了公共的API,用于在一個(gè)隊(duì)列中去存放User對(duì)象(為了簡(jiǎn)單明了,此處忽略User的具體實(shí)現(xiàn))。AbstractUserQueue則是為后續(xù)的繼承類(lèi),提供了一些公用的方法實(shí)現(xiàn)。最后的UserFifoQueue 和 UserLifoQueue,則是分別實(shí)現(xiàn)了FIFO 和 LIFO 隊(duì)列。
這是,實(shí)現(xiàn)子類(lèi)多態(tài)性的一種有效方式。但是這具體用什么來(lái)買(mǎi)我們好處呢?實(shí)際上,好處還是蠻多的。
通過(guò)創(chuàng)建一個(gè)依賴(lài)于UserQueue抽象類(lèi)型(也稱(chēng)為DI術(shù)語(yǔ)中的服務(wù))的客戶(hù)端類(lèi),可以在運(yùn)行時(shí)注入不同的實(shí)現(xiàn),無(wú)需會(huì)重構(gòu)使用客戶(hù)端類(lèi)的代碼:
public class UserProcessor {
private UserQueue userQueue;
public UserProcessor(UserQueue userQueue) {
this.userQueue = userQueue;
}
public void process() {
// process queued users here
}
}
UserProcessor展示了,注入依賴(lài)確實(shí)是IOC的一種方式。
我們可以通過(guò)一些硬編碼方式 如 new 操作,直接在構(gòu)造函數(shù)中實(shí)例化在UserProcessor中獲取對(duì)隊(duì)列的依賴(lài)關(guān)系。但是,這是典型的代碼硬編程,它引入了客戶(hù)端類(lèi)與其依賴(lài)關(guān)系之間的強(qiáng)耦合,并大大降低了可測(cè)性。耳邊警鐘聲聲想起啦!不是嗎?是的,這樣設(shè)計(jì)真的很挫。
該類(lèi)在構(gòu)造函數(shù)中聲明對(duì)抽象類(lèi) UserQueue 的依賴(lài)。也就是說(shuō),依賴(lài)關(guān)系不再通過(guò) 在構(gòu)造函數(shù)中使用 new 操作, 相反,通過(guò)外部注入的方式,要么使用依賴(lài)注入框架(如CDI和谷歌的Guice),要么使用factory或builders模式。
簡(jiǎn)而言之,使用DI,客戶(hù)端類(lèi)的依賴(lài)關(guān)系的控制,不再位于這些類(lèi)中;而是在注入器中進(jìn)行:
public static void main(String[] args) {
UserFifoQueue fifoQueue = new UserFifoQueue();
fifoQueue.add(new User("user1"));
fifoQueue.add(new User("user2"));
fifoQueue.add(new User("user3"));
UserProcessor userProcessor = new UserProcessor(fifoQueue);
userProcessor.process();
}
上述方式達(dá)到了預(yù)期效果,而且對(duì)UserLifoQueue的注入也簡(jiǎn)單明了。顯而易見(jiàn),DI確實(shí)是實(shí)現(xiàn)IOC的一種方式(該例中,DI是實(shí)現(xiàn)IOC的一個(gè)中間層)。
觀察者模式實(shí)現(xiàn)IOC
直接通過(guò)觀察者模式實(shí)現(xiàn)IOC,也是一種常見(jiàn)的直觀方式。廣義上講,通過(guò)觀察者實(shí)現(xiàn)IOC,與前文提到的通過(guò)GUI界面中的action監(jiān)聽(tīng)器方式類(lèi)似。但是在使用action監(jiān)聽(tīng)器情況下,只有在特定的用戶(hù)事件發(fā)生時(shí)(點(diǎn)擊鼠標(biāo),鍵盤(pán)或窗口事件等),才會(huì)發(fā)生調(diào)用。觀察者模式通常用于在模型視圖的上下文中,跟蹤模型對(duì)象的狀態(tài)的變遷。
在一個(gè)典型的實(shí)現(xiàn)中,一到多個(gè)觀察者綁定到可觀察對(duì)象(也稱(chēng)為模式術(shù)語(yǔ)中的主題),例如通過(guò)調(diào)用addObserver方法進(jìn)行綁定。一旦定義了被觀察者和觀察者之間的綁定,則被觀察者狀態(tài)的變遷都會(huì)觸發(fā)調(diào)用觀察者的操作。
為了深入了解這個(gè)概念,給出如下例子:
@FunctionalInterface
public interface SubjectObserver {
void update();
}
值發(fā)生改變時(shí),會(huì)觸發(fā)調(diào)用上述這個(gè)很簡(jiǎn)單的觀察者。真實(shí)情況下,通常會(huì)提供功能更豐富的API,如需要保存變化的實(shí)例,或者新舊值,但是這些都不需要觀察action(行為)模式,所以這里舉例盡量簡(jiǎn)單。
下面,給出一個(gè)被觀察者類(lèi):
public class User {
private String name;
private List<SubjectObserver> observers = new ArrayList<>();
public User(String name) {
this.name = name;
}
public void setName(String name) {
this.name = name;
notifyObservers();
}
public String getName() {
return name;
}
public void addObserver(SubjectObserver observer) {
observers.add(observer);
}
public void deleteObserver(SubjectObserver observer) {
observers.remove(observer);
}
private void notifyObservers(){
observers.stream().forEach(observer -> observer.update());
}
}
User類(lèi)中,當(dāng)通過(guò)setter方法變更其狀態(tài)事,都會(huì)觸發(fā)調(diào)用綁定到它的觀察者。
使用主題觀察者和主題,以下是實(shí)例給出了觀察方式:
public static void main(String[] args) {
User user = new User("John");
user.addObserver(() -> System.out.println(
"Observable subject " + user + " has changed its state."));
user.setName("Jack");
}
每當(dāng)User對(duì)象的狀態(tài)通過(guò)setter方法進(jìn)行修改時(shí),觀察者將被通知并向控制臺(tái)打印出一條消息。到目前為止,給出了觀察者模式的一個(gè)簡(jiǎn)單用例。不過(guò),通過(guò)這個(gè)看似簡(jiǎn)單的用例,我們了解到在這種情況下控制是如何實(shí)現(xiàn)反轉(zhuǎn)的。
觀察者模式下,主題就是起到”框架層“的作用,它完全主導(dǎo)何時(shí)何地去觸發(fā)誰(shuí)的調(diào)用。觀察者的主動(dòng)權(quán)被外放,因?yàn)橛^察者無(wú)法主導(dǎo)自己何時(shí)被調(diào)用(只要它們已經(jīng)被注冊(cè)到某個(gè)主題中的話(huà))。這意味著,實(shí)際上我們可以發(fā)現(xiàn)控制被反轉(zhuǎn)的”事發(fā)地“ – - – 當(dāng)觀察者綁定到主題時(shí):
user.addObserver(() -> System.out.println(
"Observable subject " + user + " has changed its state."));
上述用例,簡(jiǎn)要說(shuō)明了為什么,觀察者模式(或GUI驅(qū)動(dòng)環(huán)境中的action監(jiān)聽(tīng)器)是實(shí)現(xiàn)IoC的一種非常簡(jiǎn)單的方式。正是以這種分散式設(shè)計(jì)軟件組件的形式,使得控制得以發(fā)生反轉(zhuǎn)。
通過(guò)模板方法模式實(shí)現(xiàn)IoC
模板方法模式實(shí)現(xiàn)的思想是在一個(gè)基類(lèi)中通過(guò)幾個(gè)抽象方法(也稱(chēng)算法步驟)來(lái)定義一個(gè)通用的算法,然后讓子類(lèi)提供具體的實(shí)現(xiàn),這樣保證算法結(jié)構(gòu)不變。
我們可以應(yīng)用這個(gè)思想,定義一個(gè)通用的算法來(lái)處理領(lǐng)域?qū)嶓w:
public abstract class EntityProcessor {
public final void processEntity() {
getEntityData();
createEntity();
validateEntity();
persistEntity();
}
protected abstract void getEntityData();
protected abstract void createEntity();
protected abstract void validateEntity();
protected abstract void persistEntity();
}
processEntity() 方法是個(gè)模板方法,它定義了處理實(shí)體的算法,而抽象方法代表了算法的步驟,它們必須在子類(lèi)中實(shí)現(xiàn)。通過(guò)多次繼承 EntityProcessor 并實(shí)現(xiàn)不同的抽象方法,可以實(shí)現(xiàn)若干算法版本。
雖然這說(shuō)清楚了模板方法模式背后的動(dòng)機(jī),但人們可能想知道為什么這是 IoC 的模式。
典型的繼承中,子類(lèi)調(diào)用基類(lèi)中定義的方法。而這種模式下,相對(duì)真實(shí)的情況是:子類(lèi)實(shí)現(xiàn)的方法(算法步驟)被基類(lèi)的模板方法調(diào)用。因此,控制實(shí)際是在基類(lèi)中進(jìn)行的,而不是在子類(lèi)中。
這也是 IoC 的典型例子,通過(guò)分層結(jié)構(gòu)實(shí)現(xiàn)。這種情況下,模板方法只是可調(diào)的擴(kuò)展點(diǎn)的一個(gè)漂亮的名字,被開(kāi)發(fā)者用來(lái)管理自己的一系列實(shí)現(xiàn)。
總結(jié)
盡管控制反轉(zhuǎn)普遍存在于 Java 的生態(tài)系統(tǒng)中,特別是很多框架普遍采用了依賴(lài)注入,但對(duì)于多數(shù)開(kāi)發(fā)者來(lái)說(shuō),這個(gè)模式仍然很模糊,對(duì)其應(yīng)用也受限于依賴(lài)注入。在這篇文章中,我展示了幾種實(shí)際可用的實(shí)現(xiàn) IoC 的方法,闡明了這一概念。
依賴(lài)注入:從客戶(hù)端獲得依賴(lài)關(guān)系的控制不再存在于這些類(lèi)中。它存由底層的注入器 / DI 框架來(lái)處理。
觀察者模式:當(dāng)主體發(fā)生變化時(shí),控制從觀察者傳遞到主體。
模板方法模式:控制發(fā)生在定義模板方法的基類(lèi)中,而不是實(shí)現(xiàn)算法步驟的子類(lèi)中。
像往常一樣,怎樣以及何時(shí)使用 IoC 是通過(guò)對(duì)每個(gè)用例的分析來(lái)決定的,不要為了 IoC 而 IoC。
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
- Java基礎(chǔ)之Spring5的核心之一IOC容器
- Java開(kāi)發(fā)到底為什么要用 IoC 和 AOP
- 使用Java注解模擬spring ioc容器過(guò)程解析
- 基于Java反射技術(shù)實(shí)現(xiàn)簡(jiǎn)單IOC容器
- 深入理解java的spring-ioc的使用
- 輕松理解Java面試和開(kāi)發(fā)中的IoC(控制反轉(zhuǎn))
- 用java的spring實(shí)現(xiàn)一個(gè)簡(jiǎn)單的IOC容器示例代碼
- Spring學(xué)習(xí)筆記1之IOC詳解盡量使用注解以及java代碼
- 詳解JavaSE實(shí)現(xiàn)IoC
相關(guān)文章
關(guān)于SpringBoot攔截器攔截靜態(tài)資源的問(wèn)題
這篇文章主要介紹了關(guān)于SpringBoot攔截器攔截靜態(tài)資源的問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-07-07
將項(xiàng)目上傳到Maven中央倉(cāng)庫(kù)(2023最新版)
本文主要介紹了將項(xiàng)目上傳到Maven中央倉(cāng)庫(kù)(2023最新版),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2023-05-05
Spring.Net控制反轉(zhuǎn)IoC入門(mén)使用
這篇文章主要為大家詳細(xì)介紹了Spring.Net控制反轉(zhuǎn)IoC入門(mén)使用的相關(guān)資料,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-06-06
Mybatis?Plus插入數(shù)據(jù)后獲取新數(shù)據(jù)id值的踩坑記錄
在某些情況下,需要在執(zhí)行新增后,需要獲取到新增行的id,這篇文章主要給大家介紹了關(guān)于Mybatis?Plus插入數(shù)據(jù)后獲取新數(shù)據(jù)id值的相關(guān)資料,文中通過(guò)實(shí)例代碼介紹的非常詳細(xì),需要的朋友可以參考下2022-08-08
細(xì)數(shù)java for循環(huán)中的那些坑
這篇文章主要介紹了Java for循環(huán)方法,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2021-07-07

