20192306 2020-2021-1 《資料結構與面向物件程式設計》實驗七報告
依賴注入
1 概述
1.1 背景
在我們日常生活中,有一個十分形象的例子來解釋耦合,那就是時鐘。當我們拆開時鐘的後蓋可以看見三個大小不一的齒輪,它們分別帶動著時針、分針和秒針的旋轉。但時鐘的傳動裝置只有一個,所以這些齒輪之間相互齧合,一個帶動另一個旋轉,以此來共同完成一個任務。對於這樣一組齒輪傳動裝置,如果其中一個齒輪出了問題,那麼整個裝置的執行都會受到影響。齒輪組中齒輪之間的齧合關係,與軟體系統中物件之間的耦合關係非常相似,所以如果在軟體工程中採用這樣的方式來執行的話,缺點是十分明顯的。
在軟體工程中,物件之間的耦合關係是無法避免的,因為只有不同元件之間協同工作才能更好的實現一些功能。但隨著應用規模越來越大,這種物件之間的依賴關係也會隨之越來越複雜,這就會使得整個軟體的內部構造變得亂如細麻,一個元件的問題會傳遞至其他元件,問題的規模也會隨之變得,想處理這些問題也變得十分的棘手。
物件之間耦合度過高的系統,必然會出現牽一髮而動全身的情形。
耦合關係不僅會出現在物件與物件之間,也會出現在軟體系統的各模組之間,以及軟體系統和硬體系統之間。如何降低系統之間、模組之間和物件之間的耦合度,是軟體工程永遠追求的目標之一。為了解決物件之間的耦合度過高的問題,軟體專家Michael Mattson提出了IoC理論,用來實現物件之間的“解耦”,目前這個理論已經被成功地應用到實踐當中,很多的J2EE專案均採用了IoC框架產品spring。
1.2 耦合
1.2.1 緊耦合
通常情況下如果我們需要在一個物件A中使用另一個物件B,我們需要在A程式碼編寫時就完整的引用、建立相應的物件B,因此物件A就得負責物件B的整個生命週期。並且物件A的部分程式碼會依賴於建立的物件B,以此類推,當物件逐漸增多且都互相依賴時,儘管每個程式碼層級負責不同的任務,但是每個層級還是幹了一些不屬於它職責範圍的操作,這就導致了緊耦合。
1.2.2 緊耦合的影響
想象一下我們建立一個這樣的四層模型:
- 檢視層,應用的介面。它由使用者介面,控制元件比如按鈕,列表等組成。
- 表現層,處理 UI 的邏輯部分。它包含了按鈕事件呼叫的函式,UI 介面列表繫結的儲存資料的物件。
- 資料訪問層,負責與資料倉庫的互動程式碼。資料訪問層知道如何發起一個 Web 服務呼叫,然後將資料儲存到物件中,以便應用的其他模組可以方便的使用。
- 資料倉庫,獲取實際資料的地方。
且四層之間都具有耦合關係。
如果表現層涉及資料上傳,當我們上傳不同的資料格式時,需要不同的資料訪問層邏輯,即資料訪問層對應的資料庫應有所不同。這時,我們就需要在表現層例項化不同的資料訪問層模組,並通過switch函式來對不同的資料進行不同的資料訪問層處理。這樣看起來沒什麼大問題。如果表現層這時需要新增一個緩衝資料的功能(在網速受限時,提高使用者的使用體驗),你可能又需要在表現層針對緩衝與否,建立不同的資料訪問層物件。這時我們的模組涉及就違反了單一職責原則,因為我們的表現層不僅處理了UI的邏輯,還兼顧了不同資料格式資料的上傳和決定資料是否使用緩衝。
這樣的程式碼難以維護,且執行效率低下,邏輯紊亂。這時候就需要採用解耦的思想來改善程式碼。
1.2.3 解耦
解耦的步驟:
- 新增一個介面,一個抽象層,增加程式碼靈活性
- 在應用程式程式碼中加入建構函式注入
- 將解耦的各個模組組合到一起
解耦的好處:
- 解耦合的程式碼更加易於擴充套件。我們能夠在不改變大量物件的情況下增加功能。
- 我們能夠將功能獨立開來,以便編寫簡短的,易於閱讀的單元測試。
- 我們也獲得了易於維護的程式碼。當程式出錯的時候,我們能夠更加容易發現我們需要修改哪部分內容。
- 我們在團隊協作開發的過程中,比如提交合並程式碼,通常不希望也應該避免團隊成員之間的程式碼存在衝突,而解耦合有利於團隊成員各自維護自己的程式碼片段而互相不受影響。
- 解耦合可以使延遲繫結變得更加容易。延遲繫結,或者執行時繫結,是我們在執行時做決定而不是編譯時,這在特定場合下很有用。
1.3 控制反轉(IoC)
1.3.1 依賴注入
Dependency injection(DI)是一個將行為從依賴中分離的技術,簡單地說,它允許開發者定義一個方法函式依賴於外部其他各種互動,而不需要編碼如何獲得這些外部互動的例項。 這樣就在各種元件之間解耦,從而獲得乾淨的程式碼,相比依賴的硬編碼, 一個元件只有在執行時才呼叫其所需要的其他元件,因此在程式碼執行時,通過特定的框架或容器,將其所需要的其他依賴元件進行注入,主動推入。
依賴注入可以看成是 反轉控制 (inversion of control)的一個特例。反轉的是依賴,而不是其他。
依賴注入與IoC模式類似工廠模式,是一種解決呼叫者和被呼叫者依賴耦合關係的模式它解決了物件之間的依賴關係,使得物件只依賴IoC/DI容器,不再直接相互依賴,實現鬆耦合,然後在物件建立時,由IoC/DI容器將其依賴的物件注入需要呼叫的模組內,最大程度實現鬆耦合。
如圖2所示,由於引進了中間位置的“第三方”,也就是IoC容器,使得A、B、C、D這4個物件沒有了耦合關係,齒輪之間的傳動全部依靠“第三方”了,全部物件的控制權全部上繳給“第三方”IoC容器,所以,IOC容器成了整個系統的關鍵核心,它起到了一種類似“粘合劑”的作用,把系統中的所有物件粘合在一起發揮作用,如果沒有這個“粘合劑”,物件與物件之間會彼此失去聯絡,這就是有人把IoC容器比喻成“粘合劑”的由來。
軟體系統在沒有引入IoC容器之前,如果物件A依賴於物件B,那麼物件A在初始化或者執行到某一點的時候,自己必須主動去建立物件B或者使用已經建立的物件B。無論是建立還是使用物件B,控制權都在自己手上,且得負責B的生命週期。
但軟體系統在引入IOC容器之後,這種情形就完全改變了,如圖2所示,由於IoC容器的加入,物件A與物件B之間失去了直接聯絡,所以,當物件A執行到需要物件B的時候,IoC容器會主動建立一個物件B注入到物件A需要的地方。
通過前後的對比,我們不難看出來:物件A獲得依賴物件B的過程,由主動行為變為了被動行為,控制權顛倒過來了,這就是“控制反轉”這個名稱的由來。
1.3.2 IoC的好處
外部儲存通過USB介面與主機相連的例子就很像IoC,當我們需要使用外設進行儲存時,我們只需要將U盤插到USB的介面上就行了,主機需要用通過這個介面對U盤進行讀寫就行。這樣的好處是:
- U盤和主機之間只有在相連時才產生關聯,如果雙方出現故障都不會對對方產生影響。這種特性體現在軟體工程中,就是可維護性比較好,非常便於進行單元測試,便於除錯程式和診斷故障。程式碼中的每一個Class都可以單獨測試,彼此之間互不影響,只要保證自身的功能無誤即可,這就是元件之間低耦合或者無耦合帶來的好處。
- U盤廠商不需要根據不同電腦型號生產不同的U盤,兩者只需要遵守統一的USB介面標準。同類似的方式,在軟體開發過程中,每個開發團隊的成員都只需要關心實現自身的業務邏輯,完全不用去關心其它的人工作進展,通過統一的介面規範就可以實現元件的組合,大大加快了開發的速度,也提高了產品的高複用性。
- 同USB外部裝置一樣,模組具有熱插拔特性。IOC生成物件的方式轉為外接方式,也就是把物件生成放在配置檔案裡進行定義,這樣,當我們更換一個實現子類將會變得很簡單,只要修改配置檔案就可以了,完全具有熱插撥的特性。
總體而言,控制反轉的出現是為了降低軟體開發工程中不同模組之間的耦合程度。
2 例項
2.1 沒有使用IoC
下面是一段沒有使用IoC實現模組之間相互呼叫的程式碼。
/************************************************
* 這是一個書店管理系統
* BookService是管理員使用模組
* BookService需要讀取資料庫資料,所以必須例項化DataSource物件
************************************************/
public class BookService {
private DataSource dataSource = new DataSource(config);
public Book getBook(long bookId) {
try (Connection conn = dataSource.getConnection()) {
...
return book;
}
}
}
/************************************************
* UserService是使用者系統模組
* 如果UserService現在也要訪問資料庫,就必須例項化DataSource物件
************************************************/
public class UserService {
private DataSource dataSource = new DataSource(config);
public User getUser(long userId) {
try (Connection conn = dataSource.getConnection()) {
...
return user;
}
}
}
/************************************************
* CartServlet處理使用者購買
* CartServlet繼承HttpServlet
* 該類覆寫了deGet()方法
* 並傳入了HttpServletRequest和HttpServletResponse兩個物件,分別代表HTTP請求和響應。
************************************************/
public class CartServlet extends HttpServlet {
private BookService bookService = new BookService();
private UserService userService = new UserService();
protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
long currentUserId = getFromCookie(req);
User currentUser = userService.getUser(currentUserId);
Book book = bookService.getBook(req.getParameter("bookId"));
//do something else
...
}
}
CartServlet建立了BookService,在建立BookService的過程中,又建立了DataSource元件。這種模式的缺點是,一個元件如果要使用另一個元件,必須先知道如何正確地建立它。
從上面的例子可以看出,如果一個系統有大量的元件,其生命週期和相互之間的依賴關係如果由元件自身來維護,不但大大增加了系統的複雜度,而且會導致元件之間極為緊密的耦合,繼而給測試和維護帶來了極大的困難。
2.2 使用IoC
下面是一段使用IoC實現模組之間相互呼叫的程式碼。
spring不僅實現了通過容器建立元件,還實現了配置檔案內的元件之間的互相呼叫。
<!--
這是一個spring容器的配置檔案
檔案中指明瞭3個bean,對應於3個java class
-->
<beans>
<bean id="dataSource" class="DataSource" />
<bean id="bookService" class="BookService">
<property name="dataSource" ref="dataSource" />
</bean>
<bean id="userService" class="UserService">
<property name="dataSource" ref="dataSource" />
</bean>
</beans>
在IoC模式下,控制權發生了反轉,即從應用程式轉移到了IoC容器,所有元件不再由應用程式自己建立和配置,而是由IoC容器負責,這樣,應用程式只需要直接使用已經建立好並且配置好的元件。為了能讓元件在IoC容器中被“裝配”出來,需要某種“注入”機制,例如,BookService自己並不會建立DataSource,而是等待外部通過setDataSource()方法來注入一個DataSource:
public class BookService {
private DataSource dataSource;
public void setDataSource(DataSource dataSource) {
this.dataSource = dataSource;
}
}
3 總結
在軟體工程開發的工程中使用依賴注入的手段可以實現不同單元模組之間的獨立開發和測試,大大提高了開發的效率。同時依賴注入通過容器建立、控制組件,實現了元件之間的鬆耦合,大大降低了軟體的維護成本。