單一職責原則(SRP)
單一職責原則(SRP)表明一個類有且只有一個職責。一個類就像容器一樣,它能添加任意數量的屬性、方法等。然而,如果你試圖讓一個類實現太多,很快這個類就會變得笨重。任意小的改變都將導致這個單一類的變化。當你改了這個類,你將需要重新測試一遍。如果你遵守 SRP,你的類將變得簡潔和靈活。每一個類將負責單一的問題、任務或者它關注的點,這種方式你只需要改變相應的類,只有這個類需要再次測試。SRP 核心是把整個問題分為小部分,並且每個小部分都將通過一個單獨的類負責。
假設你在構建一個應用程序,其中有個模塊是根據條件搜索顧客並以Excel形式導出。隨着業務的發展,搜索條件會不斷增加,導出數據的分類也會不斷增加。如果此時將搜索與數據導出功能放在同一個類中,勢必會變的笨重起來,即使是微小的改動,也可能影響其他功能。所以根據單一職責原則,一個類只有一個職責,故創建兩個單獨的類,分別處理搜索以及導出數據。
開放封閉原則(OCP)
開放封閉原則(OCP)指出,一個類應該對擴展開放,對修改關閉。這意味一旦你創建了一個類並且應用程序的其他部分開始使用它,你不應該修改它。為什么呢?因為如果你改變它,很可能你的改變會引發系統的崩潰。如果你需要一些額外功能,你應該擴展這個類而不是修改它。使用這種方式,現有系統不會看到任何新變化的影響。同時,你只需要測試新創建的類。
假設你現在正在開發一個 Web 應用程序,包括一個在線納稅計算器。用戶可以訪問Web 頁面,指定他們的收入和費用的細節,並使用一些數學公式來計算應納稅額。考慮到這一點,你創建了如下類:
public class TaxCalculator { public decimal Calculate(decimal income, decimal deduction, string country) { decimal taxAmount = 0; decimal taxableIncome = income - deduction; switch (country) { case "India": //Todo calculation break; case "USA": //Todo calculation break; case "UK": //Todocalculation break; } return taxAmount; } }
這個方法非常簡單,通過指定收入和支出,可以動態切換不同的國家計算不同的納稅額。但這里隱含了一個問題,它只考慮了3個國家。當這個 Web 應用變得越來越流行時,越來越多的國家將被加進來,你不得不去修改 Calculate 方法。這違反了開放封閉原則,有可能你的修改會導致系統其他模塊的崩潰。
讓我們對這個功能進行重構,以符合對擴展是開放,對修改是封閉的。
根據類圖,可以看到通過繼承實現橫向的擴展,並且不會引發對其他不相關類的修改。這時 TaxCalculator 類中的 Calculate 方法會異常簡單:
public decimal Calculate(CountryTaxCalculator obj) { decimal taxAmount = 0; taxAmount = obj.CalculateTaxAmount(); return taxAmount; }
里氏替換原則(LSP)
里氏替換原則指出,派生的子類應該是可替換基類的,也就是說任何基類可以出現的地方,子類一定可以出現。值得注意的是,當你通過繼承實現多態行為時,如果派生類沒有遵守LSP,可能會讓系統引發異常。所以請謹慎使用繼承,只有確定是“is-a”的關系時才使用繼承。
假設你在開發一個大的門戶網站,並提供很多定制的功能給終端用戶,根據用戶的級別,系統提供了不同級別的設定。考慮到這個需求,設計如下類圖:
可以看到,ISettings 接口有 GlobalSettings、SectionSettings 以及 UserSettings 三個不同的實現。GlobalSettings 設置會影響整個應用程序,例如標題、主題等。SectionSettings 適用於門戶的各個部分,如新聞、天氣、體育等設置。UserSettings 為特定登錄用戶設置,如電子郵件和通知偏好。
這樣的設計沒問題,但如果有另一個需求,系統需要支持游客訪問,唯一區別是游客不支持系統的設定,為了滿足這個需求,你可能會如下設計:
public class GuestSettings : ISettings { public void GetSettings() { //get settings from database //include guest name、ip address... } public void SetSettings() { //guests are not allowed set settings throw new NotImplementedException(); } }
這樣沒問題嗎?准確來說,系統存在隱患。當單獨使用 GuestSettings 時,因為我們了解游客不能設置,所以我們潛意識並不會主動調用 SetSettings 方法。但是由於多態,ISettings 接口的實現可以被替換為 GuestSettings 對象,當調用SetSettings 方法時,可能會引發系統異常。
重構這個功能,拆分為兩個不同的接口:IReadableSettings 和 IWritableSettings。子類根據需求實現所需的接口。
接口隔離原則(ISP)
接口隔離原則(ISP)表明類不應該被迫依賴他們不使用的方法,也就是說一個接口應該擁有盡可能少的行為,它是精簡的,也是單一的。
假設你正在開發一個電子商務的網站,需要有一個購物車和關聯訂單處理機制。你設計一個接口 IOrderProcessor,它用包含一個驗證信用卡是否有效的方法(ValidateCardInfo)以及收件人地址是否有效的方法(ValidateShippingAddress)。與此同時,創建一個OnlineOrderProcessor 的類表示在線支付。
這非常好,你的網站也能正常工作。現在讓我們來考慮另一種情形,假設在線信用卡支付不再有效,公司決定接受貨到付款支付。
乍一看,這個解決方案聽起來很簡單,你可以創建一個CashOnDeliveryProcessor 並實現 IOrderProcessor 接口。貨到付款的購買方式不會涉及任何信貸卡驗證,所以,CashOnDeliveryOrderProcessor 類內部的 ValidateCardInfo 方法拋出 NotImplementedException。
這樣的設計在未來可能會出現的潛在問題。假設由於某種原因在線信用用卡付款需要額外的驗證步驟。自然,IOrderProcessor 將被修改,它將包括那些額外的方法,於此同時 OnlineOrderProcessor 將實現這些額外的方法。然而,CashOnDeliveryOrderProcessor 盡管不需要任何的附加功能,但你必須實現這些附加的功能。顯然,這違反了接口隔離原則。
你需要將這個功能重構:
新的設計分成兩個接口。IOrderProcessor 接口只包含兩個方法:ValidateShippingAddress 和 ProcessOrder,而 ValidateCardInfo 抽象到到一個單獨的接口:IOnlineOrderProcessor。現在,在線信用卡支付的任何改變只局限於IOnlineOrderProcessor 和它的子類實現,而 CashOnDeliveryOrderProcessor 是不會被影響。因此,新設計符合接口隔離原則。
依賴倒置原則(DIP)
依賴倒置原則(DIP)表明高層模塊不應該依賴低層模塊,相反,他們應該依賴抽象類或者接口。這意味着你不應該在高層模塊中使用具體的低層模塊。因為這樣的話,高層模塊變得緊耦合低層模塊。如果明天,你改變了低層模塊,那么高層模塊也會被修改。根據DIP原則,高層模塊應該依賴抽象(以抽象類或者接口的形式),低層模塊也是如此。通過面向接口(抽象類)編程,緊耦合被移除。
那么什么是高層模塊,什么是低層模塊呢?通常情況下,我們會在一個類(高層模塊)的內部實例化它依賴的對象(低層模塊),這樣勢必造成兩者的緊耦合,任何依賴對象的改變都將引起類的改變。
依賴倒置原則表明高層模塊、低層模塊都依賴於抽象,舉個例子,你現在正在開發一個通知系統,當用戶改變密碼時,郵件通知用戶。
public class UserManager { public void ChangePassword(string username,string oldpwd,string newpwd) { EmailNotifier notifier = new EmailNotifier(); //add some logic and change password //Notify the user notifier.Notify("Password was changed on "+DateTime.Now); } }
這樣的實現在功能上沒有問題,但試想一下,新的需求希望通過SNS形式通知用戶,那么我們只能手動將EmaiNorifier 替換為 SNSNotifier。在這兒,UserManager 就是高層模塊,而EmailNotifier 就是低層模塊,他們彼此耦合。我們希望解耦,依賴於抽象 INotifier,也就是面向接口的編程。