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

本文首發于我的個人博客 —— 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真是強大啊,給了用戶那么多選擇,可具體什么情況下該使用哪種注入方式和哪種配置方式呢,大雄陷入了沉思......

參考內容

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,835評論 6 534
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,676評論 3 419
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,730評論 0 380
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,118評論 1 314
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,873評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,266評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,330評論 3 443
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,482評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,036評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,846評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,025評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,575評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,279評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,684評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,953評論 1 289
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,751評論 3 394
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,016評論 2 375

推薦閱讀更多精彩內容