用小說的形式講解Spring(1) —— 為什麼需要依賴注入

大雄的練級之路:

  • 第一級:為什麼需要依賴注入
  • 第二級:注入方式哪家強
  • 第三級:配置方式如何選
  • 第四級:Spring Boot - NoXml Web Application
  • 第五級:如何給老婆解釋什麼是Restful

不斷打怪升級中...

本集概要:

  • 使用依賴注入前,代碼是什麼樣子,有什麼缺點?
  • 依賴注入是什麼?為什麼要使用依賴注入?
  • Spring如何使用xml配置的方式進行依賴注入?

大雄是一個剛踏入社會的95後,熱愛編程的他,在畢業之後進入了一家互聯網公司,負責公司內一個電商項目的開發工作。

為了讓大雄更快的成長,公司安排了哆啦作為大雄的導師。

春風得意

在哆啦的指導下,大雄很快對這個項目的代碼有了大致的了解,於是哆啦準備給大雄安排點任務。

「大雄,我們這項目現在缺少日誌列印,萬一到時上線後發現bug了,很難定位。你看看有什麼辦法可以把一些必要的信息列印到日誌文件中。」

「沒問題!」大雄爽快地答應了。

大雄以前在學校時,經常上網找各種資源,於是很快就鎖定了一個叫PerfectLogger的工具。「資料很完善,很多大神都推薦它,嗯,就用它了」。

大雄看了一下PerfectLogger的官方文檔,發現裡面提供了很多種日誌列印功能,有列印到文件的,有列印到控制台的,還有列印到遠程伺服器上的,這些類都實現了一個叫ILogger的介面:

  • ILogger
    • FileLogger
    • ConsoleLogger
    • ServerLogger

「哆啦說要列印到文件,那就用FileLogger吧!」

於是,大雄先在支付介面的代碼中,加入了日誌列印(本文使用的代碼,可以到 SpringNovel 下載):

public class PaymentAction { private ILogger logger = new FileLogger(); public void pay(BigDecimal payValue) { logger.log("pay begin, payValue is " + payValue); // do otherthing // ... logger.log("pay end"); }}

接著,大雄又在登錄、鑒權、退款、退貨等介面,都加上和支付介面類似的日誌功能,要加的地方還真不少,大雄加了兩天兩夜,終於加完了,大功告成!想到自己第一個任務就順利完成了,大雄不禁有點小得意…

改需求了

很快公司升級了系統,大雄做的日誌功能也將第一次迎來生產環境的考驗。

兩天後,哆啦找到了大雄。 「大雄,測試那邊說,日誌文件太多了,不能都列印到本地的目錄下,要我們把日誌列印到一台日誌伺服器上,你看看改動大不大。」

「這個簡單,我只需要做個全局替換,把FileLogger都替換成ServerLogger就完事了。」

哆啦聽完,皺了皺眉頭,問道,「那要是下次公司讓我們把日誌列印到控制台,或者又突然想讓我們列印到本地文件呢,你還是繼續全局替換嗎?」

大雄聽完,覺得是有點不妥……

代碼如何解耦

「我看了一下你現在的代碼,每個Action中的logger都是由Action自己創造的,所以如果要修改logger的實現類,就要改很多地方。有沒有想過可以把logger對象的創建交給外部去做呢?」

大雄聽完,覺得這好像是某種自己以前學過的設計模式,「工廠模式!」大雄恍然大悟。

很快,大雄對代碼做了重構:

public class PaymentAction { private ILogger logger = LoggerFactory.createLogger(); public void pay(BigDecimal payValue) { logger.log("pay begin, payValue is " + payValue); // do otherthing // ... logger.log("pay end"); }}

public class LoggerFactory { public static ILogger createLogger() { return new ServerLogger(); }}

有了這個LoggerFactory,以後要是要換日誌列印的方式,只需要修改這個工廠類就好了。

啪!一盤冷水

大雄高興地給哆啦提了代碼檢視的請求,但是,很快,一盤冷水就潑了過來,哆啦的回復是這樣的:

  • 工廠類每次都new一個新對象,是不是很浪費,能不能做成單例的,甚至是做成單例和多例是可以配置
  • 如果有這種需求:支付信息比較多而且比較敏感,日誌要列印到遠程伺服器,其他信息都列印到本地,怎麼實現;

大雄看完,頓時感覺自己2young2simple了,準備今晚留下來好好加班……

Spring! Spring!

正當大雄鬱悶著的時候,屏幕右下角哆啦的頭像突然蹦了出來。

「其實這種將對象交給外部去創建的機制,不僅僅是工廠模式,它還被稱為控制反轉(Inverse of Control),它還有另一個更常用的名稱,依賴注入(Dependency Injection)。這種機制,業界已經有很成熟的實現了,它就是Spring Framework,晚上早點回去,有空可以看看Spring,明天再過來改。」

那天晚上,大雄在網上找了下Spring的資料,他似乎發現了另一個世界…

使用Spring改造代碼

第二天大雄早早地就來到了公司,他迫不及待地想把原來的代碼使用Spring的方式改造一遍。

在使用gradle引入了必要的jar包後,大雄對原來的PaymentAction做了修改,不再在類內部創建logger對象,同時給PaymentAction添加了一個構造函數,方便Spring進行注入:

public class PaymentAction { private ILogger logger; public PaymentAction(ILogger logger) { super(); this.logger = logger; } public void pay(BigDecimal payValue) { logger.log("pay begin, payValue is " + payValue); // do otherthing // ... logger.log("pay end"); }}

接著創建了一個以<beans>為根節點的xml文件,引入必要的XSD文件,並且配置了兩個bean對象,使用了<constructor-arg>標籤,指定了ServerLogger作為PaymentAction構造函數的入參:

<?xml version="1.0" encoding="UTF-8"?><beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> <bean id="paymentAction" class="com.springnovel.paymentwithspringxml.PaymentAction"> <constructor-arg ref="serverLogger" /> </bean> <bean id="serverLogger" class="com.springnovel.perfectlogger.ServerLogger" /></beans>

差不多了,現在測試一下:

ApplicationContext context = new ClassPathXmlApplicationContext("payment.xml");PaymentAction paymentAction = (PaymentAction) context.getBean("paymentAction");paymentAction.pay(new BigDecimal(2));

Output:

ServerLogger: pay begin, payValue is 2ServerLogger: pay end

很棒!ServerLogger對象已經被注入到PaymentAction中了。

就這樣,大雄很快就使用Spring實現了自己昨天寫的工廠類的功能,修復了之前代碼耦合性過高的問題。

學以致用

這邊大雄正高興呢,突然發現旁邊的測試妹妹靜香眉頭緊鎖,於是過去關心了一番。

原來靜香正在測試一個刪除訂單的功能,但是現在測試用的資料庫突然掛了,導致靜香不能進行測試。

大雄看了看訂單刪除介面的代碼:

public class OrderAction { public void deleteOrder(String orderId) { // 鑒權 // 此處略去一萬字... IOrderDao orderDao = new OrderDao(); orderDao.deleteOrder(orderId); }}

「這又是一個代碼耦合過緊的問題!」大雄脫口而出。

「這個刪除訂單的介面有幾個邏輯:鑒權、刪除、回滾等,但是這裡把刪除的資料庫操作和OrderDao綁定死了,這樣就要求測試這個介面時必須要連接到資料庫中,但是作為單元測試,我們只是想測刪除訂單的邏輯是否合理,而訂單是否真的刪除,應該屬於另一個單元測試了」 大雄很是激動,嘴裡唾沫橫飛。

「我來幫你改一下。」

「控制反轉」後的OrderAction:

public class OrderAction { private IOrderDao orderDao; public OrderAction(IOrderDao orderDao) { super(); this.orderDao = orderDao; } public void deleteOrder(String orderId) { // 鑒權 // 此處略去一萬字... orderDao.deleteOrder(orderId); }}

改造後的OrderAction,不再和OrderDao這個實現類耦合在一起,做單元測試的時候,可以寫一個「Mock」測試,就像這樣:

public void mockDeleteOrderTest() { IOrderDao orderDao = new MockOrderDao(); OrderAction orderAction = new OrderAction(orderDao); orderAction.deleteOrder("1234567@#%^$");}

而這個MockOrderDao是不需要連接資料庫的,因此即便資料庫掛了,也同樣可以進行單元測試。

一旁的哆啦一直在靜靜地看著,然後拍了拍大雄的肩膀,「晚上請你和靜香去擼串啊」,說完,鬼魅的朝大雄挑了挑眉毛。

大雄的筆記

這兩天大雄可謂是收穫頗豐,見識了依賴注入的必要性,還了解了如何使用Spring實現依賴注入。擼完串後,回到家,大雄在記事本上寫下了心得:

  • 為什麼要使用依賴注入
    • 傳統的代碼,每個對象負責管理與自己需要依賴的對象,導致如果需要切換依賴對象的實現類時,需要修改多處地方。同時,過度耦合也使得對象難以進行單元測試。
    • 依賴注入把對象的創造交給外部去管理,很好的解決了代碼緊耦合(tight couple)的問題,是一種讓代碼實現松耦合(loose couple)的機制。
    • 松耦合讓代碼更具靈活性,能更好地應對需求變動,以及方便單元測試
  • 為什麼要使用Spring
    • 使用Spring框架主要是為了簡化Java開發(大多數框架都是為了簡化開發),它幫我們封裝好了很多完善的功能,而且Spring的生態圈也非常龐大。
    • 基於XML的配置是Spring提供的最原始的依賴注入配置方式,從Spring誕生之時就有了,功能也是最完善的(但是貌似有更好的配置方法,明天看看!)。

未完待續

寫完筆記,大雄繼續看之前只看了一小部分的Spring指南,他發現除了構造器注入,還有一種注入叫set注入;除了xml配置,還可以使用註解、甚至是Java進行配置。Spring真是強大啊,給了用戶那麼多選擇,可具體什麼情況下該使用哪種注入方式和哪種配置方式呢,大雄陷入了沉思……

參考內容

  • 《Spring in Action》
  • tutorialspoint - Spring Tutorial
  • javatpoint - Spring Tutorial
  • Why does one use dependency injection?
  • Dependency Injection and Unit Testing

推薦閱讀:

周末薦書 | 《Spring實戰》(第4版)
實體類的欄位的驗證應該寫在service層嗎?
如何學好ssh框架,spring學起來怎麼這麼難呢?
MyBatis不是完整的ORM框架?

TAG:Spring | 依赖注入 | Java |