--- tags: 學習筆記, 架構 --- # 淺談依賴注入、控制反轉以及代理模式 ## Dependency Injection:依賴注入 ### 依賴 依賴指的是物件導向中的繼承、實現、擁有等關係。 ![依賴示意](https://i.imgur.com/wEpKHUo.png) > A,C,D 擁有 B,A,C,D 都對 B 有所依賴 ```java public class A { B b = new B(); ... } ``` ### 注入 ![注入示意](https://i.imgur.com/DXUFEna.png) > 注入的意思是從外部傳入。 ```java public class A { B b; //建構元注入 public A(B _b) { this.b = _b; } //設值注入 public void a(B _b) { this.b = _b; ... } } ``` ## Inversion of Control:控制反轉 重新檢視一下[依賴章節裡的示意](#依賴),不難發現假設 Main 針對 A,C,D 使用的 B 做些改變: 在原本的狀況中由於物件依賴的關係就必須深入 A,C,D 調整實例化 B 的方法(或是另外設計 A',C',D')。 這種**強烈的耦合,就會對未來程式的調整造成極大的困難**,也因此我們會希望針對這個依賴的狀況做一個反轉,讓依賴關係轉換過來。 ![醫生注射圖](https://i.imgur.com/h4bU5N4.png) 而其中一種實現方式,就是依賴注入: [注入章節裡的示意](#注入)改採注入的模式,Main 可以直接決定好自己對 B 的實例後再將其注入 A、C、D 迫使 A、C、D 都必須如此使用。 ### 好處 簡化複雜物件的實例過程,以三層式架構為例,想一下多層依賴 ![3 Tier](https://i.imgur.com/PIM6V3K.png) 容易造成服務元件的多次實例化: - D Service 的實例化需要實例化 C、E - B Service 也需要實例化一個 C 實際上對於服務元件,應該不需實例化如此多次,期望應以 Singleton 的模式處理。 透過 DI、IoC 可以調整成下面模式,簡化這個初始化的過程。 ![Spring IOC 示意](https://i.imgur.com/TyJcein.png) ## Proxy Design Pattern:代理模式 說明代理模式,我們可以幻想一個 Sales 賣東西的需求 我們做了一個 Salesman,並實作了他賣東西的方法。 ```java interface Sales { String doSale(); } class Salesman implements Sales { private final BigDecimal productAmount = BigDecimal.valueOf(100); @Override public BigDecimal doSale(BigDecimal amount) throws Exception { if (amount == null) { throw new RuntimeException("Not enough money"); } if (productAmount.compareTo(amount) <= 0) { return amount.subtract(productAmount); } else { throw new Exception("Not enough money"); } } } ``` 接下來我有一個額外的需求需要讓所有這個 doSale 的過程都被記錄下來,該怎麼做呢? 這時候可怕的設計就出來了,直接對這個程式穿插一堆不關於 doSale 的邏輯在裡面,這是很可怕的! ### Static Proxy:靜態代理 另一個比較優秀的方式,是實作一個針對 doSale 的 log 記錄物件,由這個物件來負責記錄 log 並協助觸發 doSale。 換言之,當我們要 doSale 時不再直接接觸 Salesman,而是透過 SalesLiveLog **代理**。 ```java class SalesLiveLog implements Sales { private final static Logger LOGGER = LoggerFactory.getLogger(SalesLiveLog.class); private final Salesman salesman; public SalesLiveLog(Salesman salesman) { this.salesman = salesman; } @Override public BigDecimal doSale(BigDecimal amount) throws Exception { LOGGER.info("sales will received $ {}", amount.toPlainString()); BigDecimal result = salesman.doSale(amount); LOGGER.info("sales return $ {}", result.toPlainString()); return result; } } ``` 以上說明的例子,也就是靜態代理的做法,可以看出整個代理行為是伴隨著程式開發的過程,在實際執行前就確定下來的。 然而所有的代理都需要這樣土炮的去建立的話,雖然依然達成了職務的分離、關注點的切割,但好像未免有點太累了? 想想看這種 log 記錄的需求、最外層的例外捕獲處理、RDBMS 的交易事務控制等,到底需要貫穿多少程式與功能? 再想想看,各大語言都有的諸多框架,這些框架總是在我們做了一些特定行為時於背地裡偷偷幫我們達成一些重要的作業(連線池的控管分配、交易事務的控制等),簡化開發階段時的負擔。 他們是怎麼實現的? ### Dynamic Proxy:動態代理 ![動態代理](https://i.imgur.com/3pCO3rZ.png) 現代許多語言都有了反射的機制,透過這個功能我們就有辦法做到在執行階段時透過設計好的邏輯,自動的去觸發所需的代理功能。 具體來說,透過反射我們可以取得任意 Object 的屬性、類別等資訊,亦可以透過反射去觸發執行方法。 因此只要能夠讓我們在所有物件方法被呼叫時去取得他的呼叫者、被呼叫方法、輸入參數等資訊。 我們就可以在這個階段針對呼叫者判斷需要觸發哪種代理,完成代理後再去執行被呼叫方法並將回傳值拋出。 而很多框架功能的實現其實就是基於這一層又一層設計好的動態代理類來自動觸發執行的。 ## AOP(Aspect-Oriented Programming):面向切面編程 這是一種概念與一系列標準,在上面探討的一系列主題中,最後其實就是想做到一件事情:對原程式的補充,關注點分離思考。 AOP 是 OOP 的延續,是軟件開發中的一個熱點,也是 Spring 框架中的一個重要內容,是函數式編程的一種衍生范型。利用 AOP 可以對業務邏輯的各個部分進行隔離,從而使得業務邏輯各部分之間的耦合度降低,提高程序的可重用性,同時提高開發的效率。 ### 作用 在程序運行期間,在不修改源碼的情況下對方法進行功能增強優勢:減少重復代碼,提高開發效率,並且便於維護。 ### Spring AOP 的實現 Spring 的 AOP 實現底層就是透過 cglib 對上面動態代理的程式進行封裝,封裝後我們只需要對需要關注的部分進行代碼編寫,並通過配置的方式完成指定目標的方法增強。 #### 角色 - Target(目標對象):代理的目標對象。 - Proxy(代理):一個類被 AOP 織入增強後,就產生一個結果代理類。 - Joinpoint(連接點):所謂連接點是指那些被攔截到的點。在 Spring 中,這些點指的是方法。 - Pointcut(切入點):所謂切入點是指我們要對哪些 Joinpoint 進行攔截的定義(被增強的方法)。 - Advice(通知/增強):所謂通知是指攔截到 Joinpoint 之後所要做的事情就是通知(對目標對象增強的方法)。 - Aspect(切面):是切入點和通知(引介)的結合(目標方法+增強=切面) - Weaving(織入):是指把增強應用到目標對象來創建新的代理對象的過程, Spring 采用動態代理織入,而 AspectJ 采用編譯期織入和類裝載期織入(一個動作,切點和通知結合的過程=織入) #### 過程 ![Spring AOP 過程](https://i.imgur.com/rT7KiMP.png) #### 限制 基於動態代理實作的 AOP,其實就也受限於動態代理的發動,亦即只有跨類別的互相呼叫才會被攔截到(同類別內不同 method 的互相呼叫,是無法觸發的)。 ```java interface Sales { String doSale(); } class Salesman implements Sales { private final BigDecimal productAmount = BigDecimal.valueOf(100); @Override public BigDecimal doSale(BigDecimal amount) throws Exception { if (amount == null) { throw new RuntimeException("Not enough money"); } if (productAmount.compareTo(amount) <= 0) { return amount.subtract(productAmount); } else { throw new Exception("Not enough money"); } // 類別內部呼叫,不會觸發 AOP 攔截 doSomething(); } public void doSomething() { ... } } class SalesLiveLog implements Sales { private final static Logger LOGGER = LoggerFactory.getLogger(SalesLiveLog.class); private final Salesman salesman; public SalesLiveLog(Salesman salesman) { this.salesman = salesman; } @Override public BigDecimal doSale(BigDecimal amount) throws Exception { LOGGER.info("sales will received $ {}", amount.toPlainString()); // SalesLiveLog 呼叫 salesman 的方法,可以被攔截 BigDecimal result = salesman.doSale(amount); LOGGER.info("sales return $ {}", result.toPlainString()); return result; } } ```