[ASP.NET]重構之路系列v4 – 簡單使用interface之『你也會IoC』

[ASP.NET]重構之路系列v4 – 簡單使用interface之『你也會IoC』

前言
上次
v3版本,我們將Entity, Service, Dao, Utility都放到了類別庫裡面,讓我們可以輕鬆的在不同專案中用同一份組件。雖然文章沒有獲得太多的讚賞,不過相信那一定是太多人會這一招了。如果您已經會了,恭喜你,這是很重要的一步,沒有類別庫,後面我們很多事情都不容易實作出來。

今天要講的運用是interface,相信很多人都還是interface苦手,大部分的人還是卡在『為什麼我要用interface』,當我帶出可惡的PM需求時,大家應該會感同身受,而且覺得相當熟悉。跟著文章中的步伐前進,您將會知道,原來interface運用可以這麼簡單,
這麼有用!


需求說明
越來越可惡的PM提出了另外一個需求:『上次您將商業邏輯跟資料存取放到了類別庫,讓我們的批次可以一起使用,這個idea實在太棒了!我們現在有另一個網站,也有個Validate的頁面,也想使用AuthenticationService,不過我們網站後面的資料庫都是Oracle的,資料結構也不一樣,那可以用同一份AuthenticationService嗎?』

當然可以!讀完這篇文章之後,希望您也可以這樣大聲的跟PM講:『當然可以!』。

先簡單列出,我們的功能需求:

  1. 頁面一樣
  2. 商業邏輯一樣
  3. DB來源不一樣


我們先來看「通常」大家拿到這一份需求,可能會怎麼做:

簡單嘛,我們多傳一個參數給AuthenticationService,來判斷是哪一個網站呼叫的,如果是Oracle的網站,就換呼叫Oracle的Dao方法。如果是原本的SQL server網站,就呼叫原本SQL的Dao方法。一個步驟就解決了,帥吧!

所以我們的程式就會變成這樣:

AuthenticationService: (很聰明的用了之前學到的手法)
image

Validate.aspx.cs
image

Console的Main()
image

接著就會發現,原本用到AuthenticationService.VerifyPasswordById都要改,都要新增一個參數: site,這對我們來說很困擾,為什麼我為了一個新網站的需求,卻要『大幅』修改原本使用這個Service的程式。(您可能在很多地方都用到這個Service),完全違背了開放-封閉原則。或許您是使用VB.NET的,會說『簡單啊,我用optional來標示這個參數,那我就可以只為了新的Oracle網站來滿足新的需求即可。

為了這樣的需求,而採用了optional來標示參數,是一種慢性毒藥。會逐漸腐蝕您系統的架構到無法自拔。當optional參數個數超過4個的時候,您就會發現這個service方法的邏輯根本沒有可維護性。這樣的設計會導致內聚力太低,同樣的service甚至同一個方法裡面,包含了太多混雜的職責,所以隨便新增一個需求,就會讓程式動彈不得,越陷越深。

山不轉路轉,另一種常見的作法也是種毒藥,我們新增一個Service的方法,讓Oracle的Website呼叫不就得了?這樣之前的Code就不用改啦,又可以滿足新的需求。

程式如下:

image

重構後:
image

這樣不是很簡單明瞭嗎?

我們來看Oracle Website在使用的時候:

image

在用這個類別庫的人,一定會有這個疑問,這兩個方法有什麼不同?這會讓職責混淆,使用上容易誤用。還有一個很嚴重的問題,萬一以後是從Excel檔案來呢?從Access來呢?從txt檔來呢?從其他web service來呢?越來越多的需求,我們的Architecture就會越蓋越歪,最後垮下來而無法彌補。

那,我們該怎麼解決這個問題?對,用Interface!!

設計步驟:
先把剛剛的code都砍掉(笑)! 我們重新思考一下,原本PM提出來的需求是,只有資料存取的部分不一樣,但『商業邏輯的部分完全一樣』,我們希望可以多一個資料存取功能是滿足新的需求。也就是給Oracle website用的仍然是同一個AuthenticationService的VerifyPasswordById的方法,這是不能變也不想變的。而對於Oracle的資料存取方法,也仍然需要傳入id,才能得到對應的password。

步驟一:
我們先在原本的AuthenticationDao的QueryPasswordById方法上,按滑鼠右鍵=>重構=>擷取介面。

image

把我們的QueryPasswordById方法打勾,按下確定。
image 

接著我們原本的AuthenticationDao後面就會多出來
: IAuthenticationDao
image 

而產生的介面也相當簡單:

image

步驟二:
新增一個AuthenticationDaoForOracle的類別在DataAccess的folder底下,實作IAuthenticationDao:

image

會看到Visual Studio自動幫我們產生了要實作(遵守)介面的方法:
image 

接著我們就可以不理它了!(笑)

步驟三:
接著來調整我們的AuthenticationService,很簡單地!我們將原本public的MyAuthenticationDao的Property,將型別從AuthenticationDao改成IAuthenticationDao。讓service原本直接呼叫AuthenticationDao的相依性,轉成相依於IAuthenticationDao這個介面上,而不直接相依於某一個特定類別。

image

這個時候,其實我們的方案,建置是會成功的。我們的所有邏輯也都撰寫完畢了,是的,就這麼簡單。我們已經滿足了,service用同一份,Dao資料來源不同的設計了,接著,我們只是要做組合的動作。

步驟四:
回到我們原本有用到AuthenticationService的程式中,我們要多做一件事:將我們要用的Dao(也就是concrete class),塞給AuthenticationService。請各位想像一下,當我們在步驟三,將AuthenticationService開了一個介面出來給外面,就像一塊拼圖開一個特定的凹洞出來。有實作這個介面的class,就能滿足這個凹洞,他們就可以組合在一起,發揮不同的功能。

image

接著,我們來設定一下中斷點,看一下程式是否跟原本一樣,是使用AuthenticationDao來存取資料:
image

image

image 

大家想像自己的程式,就像以前的
聖戰士,或是百獸王,我們的程式,就是一個一個的元件,用的人可以任意的組合他們,只要能夠『插』(injection)的起來。

最後,我們也將Oracle website的程式修改一下。我們希望在Oracle的website,使用AuthenticationService的時候,後面是接著AuthenticationDaoForOracle這個元件的。

image

當執行偵錯,就會看到最後是進入AuthenticationDaoForOracle的中斷點:
image 

最後我們的程式架構如下圖所示,正規來說,Service也應該要有對應的interface,讓頁面可以只相依於Service的Interface,讓Service也可以抽換。最後就達成我們3-layer: Presentation layer (頁面、UI), Business logic layer(Service class), Persistence layer(Data access object),都有透過interface來隔絕layer與layer之間的相依性,讓我們的系統架構可以有彈性的抽換,以及無限的擴充性,也可以滿足開放-封閉原則。

image

步驟三補充說明:
步驟三中,我們提到將原本的public propery型別直接改成介面,並交給外面來set。這『可能』會導致一個問題,就是當外界使用AuthenticationService,卻沒有assign MyAuthenticationDao的時候,會出現NullReferenceException。就像使用的人沒有告知AuthenticationService後面要使用的元件,導致方法走到後面就斷掉了。

雖然會出現這樣的潛在問題,但這樣設計是很合理的使用狀況。如果真的要限制,不能出現這類的狀況,也就是強迫使用這個Service的人,一定要assign MyAuthenticationDao,我們可以在AuthenticationService的建構式,加入IAuthenticationDao的參數,讓使用AuthenticationService的場景,在new的時候,一定要給IAuthenticationDao的concrete class。

image

有人或許會說,如果在建構式中assign了IAuthenticationDao的concrete class的instance,那MyAuthenticationDao這個屬性是不是就可以乾脆開成private,基本上,是!

這兩種作法,哪一種都可以,但大家可以想像,如果我這個Service用到很多外部類別,那麼我的建構式不就超長一串?是的,而且這是合理的情況。為了節省每次要用,都要new一堆concrete class的instance塞給我們要用的service,所以會有DI framework的出現。(DI=Dependency injection),透過DI framework,我們可以把『組合』這件事,寫的更輕鬆,而且獨立出來統一管理,不會散落一地。而DI framework,有的有支援auto-wiring,也就是framework在碰到建構式有需要的型別時,會自動填入你設定好的concrete class的instance。有的有支援injection public property。所以,採用哪一種寫法,其實可以因應不同的DI framework來設計,基本上兩種都OK啦。

結論
透過上面的需求跟實際操作,相信大家已經知道為什麼我們要使用interface,以及使用上的概念就像組裝一樣。這也是為什麼interface通常會被解釋成『合約』的概念,因為實作了這個合約,這個class就要做出凸出來的那一塊,只要有人有一樣凹的情況,就要能拿這個凸的class去接。

  1. 使用了interface,其實間接的就是實作了IoC的概念。原本我們的Authentication.Verify(),裡面用到QueryPasswordById()是相依於AuthenticationDao上。透過介面,我們的AuthenticationService是相依於IAuthenticatoinDao介面上。這就是IoC(控制反轉)的概念。
    這樣的設計,我們相依的這個介面,就像一個凹口,後面可以有很多很多種凸出來的class來接,這樣我們在使用時就可以任意組裝。

    image

  2. 使用Interface可以讓關注點分離,讓設計的邏輯穩定。我們的系統結構變成下圖所示:
    image

    image
     
  3. 除了讓原本的邏輯可以穩定不變以外,透過Interface,更為未來的無限擴充奠下了穩固的架構:
    image 
  4. 增加可測試性。(這個就留到後面的重構再來談囉…)


Sample Code:RefactoringSample-v4.zip
 

註:(IoC的概念可以參考:[Software Architecture]IoC and DI

注意:本篇IoC的觀念不夠精確,請參考:[30天快速上手TDD][Day 5]如何隔離相依性 - 基本的可測試性更為精準。


blog 與課程更新內容,請前往新站位置:http://tdd.best/