本文首發于我的個人博客 —— Bridge for You,轉載請標明出處。
本集概要:
- 使用依賴注入前,代碼是什么樣子,有什么缺點?
- 依賴注入是什么?為什么要使用依賴注入?
- 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 2
ServerLogger: 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”測試,就像這樣:
@Test
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真是強大啊,給了用戶那么多選擇,可具體什么情況下該使用哪種注入方式和哪種配置方式呢,大雄陷入了沉思......