上次v3版本,我們將Entity, Service, Dao, Utility都放到了類別庫裡面,讓我們可以輕鬆的在不同專案中用同一份組件。雖然文章沒有獲得太多的讚賞,不過相信那一定是太多人會這一招了。如果您已經會了,恭喜你,這是很重要的一步,沒有類別庫,後面我們很多事情都不容易實作出來。
這篇要講的運用是interface,相信很多人都還是interface苦手,大部分的人還是卡在『為什麼我要用interface』,當我帶出可惡的PM需求時,大家應該會感同身受,而且覺得相當熟悉。跟著文章中的步伐前進,您將會知道,原來interface運用可以這麼簡單,這麼有用!
需求說明
越來越可惡的PM提出了另外一個需求:『上次您將商業邏輯跟資料存取放到了類別庫,讓我們的批次可以一起使用,這個idea實在太棒了!我們現在有另一個網站,也有個Validate的頁面,也想使用AuthenticationService,不過我們網站後面的資料庫都是Oracle的,資料結構也不一樣,那可以用同一份AuthenticationService嗎?』
當然可以!讀完這篇文章之後,希望您也可以這樣大聲的跟PM講:『當然可以!』。
[如何提升系統品質]系列文章連結
先列出功能需求:
1.頁面一樣
2.商業邏輯一樣
3.DB來源不一樣
先來看「通常」大家拿到這一份需求,可能會怎麼做:
簡單嘛,多傳一個參數給Service,來判斷是哪一個網站呼叫的,如果是Oracle,就呼叫Oracle的方法。如果是原本的SQL server網站,就呼叫原本SQL的方法。
程式就變成這樣:
Service:
Validate.aspx.cs
Console的Main()
接著就會發現,原本用到Service.VerifyPasswordById都要改,都要新增一個參數: site,這很困擾,為什麼為了一個新網站的需求,卻要『大幅』修改原本使用這個Service的程式。完全違背了開放-封閉原則。或許您會說『簡單啊,我用optional來標示這個參數,那我就可以只為了新的Oracle網站來滿足新的需求即可。
為了這樣的需求,而採用了optional來標示參數,是一種慢性毒藥。會逐漸腐蝕您系統的架構到無法自拔。當optional參數個數超過4個的時候,您就會發現這個service方法的邏輯根本沒有可維護性。這樣的設計會導致內聚力太低,同樣的service甚至同一個方法裡面,包含了太多混雜的職責,所以隨便新增一個需求,就會讓程式動彈不得,越陷越深。
另一種常見的作法也是種毒藥,新增一個Service的方法,讓Oracle的Website呼叫不就得了?這樣之前的Code就不用改啦,又可以滿足新的需求。
程式如下:
重構後:
這樣不是很簡單明瞭嗎?
來看Oracle Website使用的時候:
用這個類別庫的人,一定會有疑問,這兩個方法有什麼不同?這會讓職責混淆,使用上容易誤用。還有個很嚴重的問題,萬一以後是從Excel檔案來,從Access來,從txt檔來,從其他web service來呢?越來越多的需求,Architecture就會越蓋越歪,最後垮下來而無法彌補。
那,該怎麼解決這個問題?對,用Interface!!
設計步驟:
先把剛剛的code都砍掉(笑)! 重新思考一下,原本PM提出來的需求是,只有資料存取的部分不一樣,但『商業邏輯的部分完全一樣』,我們希望可以多一個資料存取功能滿足新的需求。也就是給Oracle website用的仍然是同一個Service的VerifyPasswordById的方法,這是不能變也不想變的。而對於Oracle的方法,也仍然需要傳入id,才能得到password。
步驟一:
先在QueryPasswordById方法上,按滑鼠右鍵=>重構=>擷取介面。
把方法打勾,按下確定。
接著AuthenticationDao後面就會多出來 : IAuthenticationDao
產生的介面也相當簡單:
步驟二:
新增一個AuthenticationDaoForOracle的類別,實作IAuthenticationDao:
會看到VS自動產生了要實作(遵守)介面的方法:
接著就可以不理它了!(笑)
步驟三:
接著調整Service,將原本public的MyAuthenticationDao的Property,將型別從AuthenticationDao改成IAuthenticationDao。讓service原本直接呼叫AuthenticationDao的相依性,轉成相依於IAuthenticationDao這個介面上,而不直接相依於某一個特定類別。
這時其實方案建置是會成功的。所有邏輯也都撰寫完畢了,是的,就這麼簡單。我們已經滿足了,service用同一份,Dao資料來源不同的設計了,接著只是要做組合的動作。
步驟四:
回到原本有用到Service的程式中,要多做一件事:將要用的Dao(也就是concrete class),塞給Service。請各位想像一下,當在步驟三,將Service開了一個介面出來給外面,就像一塊拼圖開一個特定的凹洞出來。有實作這個介面的class,就能滿足這個凹洞,他們就可以組合在一起,發揮不同的功能。
接著,來設定一下中斷點,看一下程式是否跟原本一樣,是使用AuthenticationDao來存取資料:
大家想像程式,就是一個一個的元件,用的人可以任意的組合,只要能夠『插』(injection)的起來。
最後將Oracle website的程式修改一下。希望在Oracle的website,使用Service的時候,後面是接著AuthenticationDaoForOracle這個元件的。
當執行偵錯,就會看到最後是進入AuthenticationDaoForOracle的中斷點:
最後架構如下圖所示,正規來說,Service也應該要有對應的介面,讓頁面可以只相依於Service的介面,讓Service也可以抽換。最後就達成我們3-layer: Presentation layer (頁面、UI), Business logic layer(Service class), Persistence layer(Data access object),都透過介面來隔絕layer與layer之間的相依性,讓架構可以彈性抽換,具備擴充性,也可以滿足開放-封閉原則。
結論
透過上面的需求跟實際操作,相信大家已經知道為何要使用介面,以及使用上的概念就像組裝一樣。這也是為什麼介面通常會被解釋成『合約』的概念,因為實作了這個合約,這個class就要做出凸出來的那一塊,只要有人有一樣凹的情況,就要能拿這個凸的class去接。
1.使用了介面,其實間接的就是實作了IoC的概念。原本的Authentication.Verify(),裡面用到QueryPasswordById()是相依於AuthenticationDao上。透過介面,Service是相依於IAuthenticatoinDao介面上。這就是IoC(控制反轉)的概念。
這樣的設計,相依的這個介面,就像一個凹口,後面可以有很多很多種凸出來的class來接,這樣在使用時就可以任意組裝。
2.使用介面可以讓關注點分離,讓設計的邏輯穩定。系統結構變成下圖所示:
3.除了讓原本的邏輯可以穩定不變以外,透過介面,更為未來的無限擴充奠下了穩固的架構:
4.增加可測試性。