編碼最佳實踐——介面分離原則
在面向物件程式設計中,介面是一個非常重要的武器。介面所表達的是客戶端程式碼需求和需求具體實現之間的邊界。介面分離原則主張介面應該足夠小,大而全的契約(介面)是毫無意義的。
介面分離的原因
將大型介面分割為多個小型介面的原因有:
①需要單獨修飾介面
②客戶端需要
③架構需要
需要單獨修飾介面
我們通過拆解一個單個巨型介面到多個小型介面的示例,分離過程中建立了各種各樣的修飾器,來講解大量應用介面分離原則帶來的主要好處。
下面這個介面包含了5個方法,用於使用者對實體物件的持久化儲存進行CRUD操作。
public interface ICreateReadUpdateDelete<TEntity> { void Create(TEntity entity); TEntity ReadOne(Guid identity); IEnumerable<TEntity> ReadAll(); void Update(TEntity entity); void Delete(TEntity entity); } 複製程式碼
ICreateReadUpdateDelete是一個泛型介面,可以接受不同的實體型別。客戶端需要首先宣告自己要依賴的TEntity。CRUD中的每個操作都是由對應的ICreateReadUpdateDelete介面實現來執行,也包括修飾器實現。
有些修飾器作用於所有方法,比如日誌修飾器。當然,日誌修飾器屬於橫切關注點,為了避免在多個介面中重複實現,也可以使用面向切面程式設計(AOP)來修飾介面的所有實現。
public class CrudLogging<TEntity> : ICreateReadUpdateDelete<TEntity> { private readonly ICreateReadUpdateDelete<TEntity> decoratedCrud; private readonly ILog log; public CrudLogging(ICreateReadUpdateDelete<TEntity> decoratedCrud, ILog log) { this.decoratedCrud = decoratedCrud; this.log = log; } public void Create(TEntity entity) { log.InfoFormat("Create entity of type {0}", typeof(TEntity).Name); decoratedCrud.Create(entity); } public void Delete(TEntity entity) { log.InfoFormat("Delete entity of type {0}", typeof(TEntity).Name); decoratedCrud.Delete(entity); } public IEnumerable<TEntity> ReadAll() { log.InfoFormat("Reading all entities of type {0}", typeof(TEntity).Name); return decoratedCrud.ReadAll(); } public TEntity ReadOne(Guid identity) { log.InfoFormat("Readingentity of type {0}", typeof(TEntity).Name); return decoratedCrud.ReadOne(identity); } public void Update(TEntity entity) { log.InfoFormat("Updateentity of type {0}", typeof(TEntity).Name); decoratedCrud.Update(entity); } } 複製程式碼
但是有些修飾器只應用於介面的部分方法上,而不是所有的方法。假設現在有這麼一個需求,在持久化儲存中刪除某個實體前提示使用者。切記不要直接去修改現有的類實現,因為這會違背開放與封閉原則。相反,應該建立一個客戶端用來刪除實體的新實現。
public class DeleteConfirm<TEntity> : ICreateReadUpdateDelete<TEntity> { private readonly ICreateReadUpdateDelete<TEntity> decoratedCrud; public DeleteConfirm(ICreateReadUpdateDelete<TEntity> decoratedCrud) { this.decoratedCrud = decoratedCrud; } public void Create(TEntity entity) { decoratedCrud.Create(entity); } public IEnumerable<TEntity> ReadAll() { return decoratedCrud.ReadAll(); } public TEntity ReadOne(Guid identity) { return decoratedCrud.ReadOne(identity); } public void Update(TEntity entity) { decoratedCrud.Update(entity); } public void Delete(TEntity entity) { Console.WriteLine("Are you sure you want to delete the entity ? [y/n]"); var keyInfo = Console.ReadKey(); if(keyInfo.Key == ConsoleKey.Y) { decoratedCrud.Delete(entity); } } } 複製程式碼
如上程式碼,DeleteConfirm只修飾了Delete方法,其餘方法都是 直託方法 (沒有任何修飾,就像直接呼叫被修飾的介面方法一樣)。儘管這些直託方法什麼都沒有做,你還是需要一一實現,並且還需要編寫測試方法驗證方法行為是否正確,這樣做與介面分離的方式比較起來麻煩的多。
我們可以將Delete方法從ICreateReadUpdateDelete介面分離,這樣會得到兩個介面:
public interface ICreateReadUpdate<TEntity> { void Create(TEntity entity); TEntity ReadOne(Guid identity); IEnumerable<TEntity> ReadAll(); void Update(TEntity entity); } public interface IDelete<TEntity> { void Delete(TEntity entity); } 複製程式碼
然後只對IDelete介面提供確認修飾器的實現:
public class DeleteConfirm<TEntity> : IDelete<TEntity> { private readonly IDelete<TEntity> decoratedDelete; public DeleteConfirm(IDelete<TEntity> decoratedDelete) { this.decoratedDelete = decoratedDelete; } public void Delete(TEntity entity) { Console.WriteLine("Are you sure you want to delete the entity ? [y/n]"); var keyInfo = Console.ReadKey(); if(keyInfo.Key == ConsoleKey.Y) { decoratedDelete.Delete(entity); } } } 複製程式碼
這樣一來,程式碼意圖更清晰,程式碼量減少了,也沒有那麼多的直託方法,相應的測試工作量也變少了。
客戶端需要
客戶端只需要它們需要的東西。那些巨型介面傾向於給使用者提供更多的控制能力,帶有大量成員的介面允許客戶端做很多操作,甚至包括它們不應該做的。更好的辦法是儘早採用防禦方式進行程式設計,以此阻止其他開發人員(包括將來的自己)無意中使用你的介面做出一些不該做的事情。
現在有一個場景是通過使用者配置介面訪問程式當前的主題,實現如下:
public interface IUserSettings { string Theme { get; set; } } 複製程式碼
public class UserSettingsConfig : IUserSettings { private const string ThemeSetting = "Theme"; private readonly Configuration config; public UserSettingsConfig() { config = ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.None); } public string Theme { get { return config.AppSettingd[ThemeSetting].value; } set { config.AppSettingd[ThemeSetting].value = value; config.Save(); ConfigurationManager.RefreshSection("appSettings"); } } } 複製程式碼
介面不同的客戶端以不同的目的使用同一個屬性:
public class ReadingController { private readonly IUserSettings userSettings; public ReadingController(IUserSettings userSettings) { this.userSettings = userSettings; } public string GetTheme() { return userSettings.Theme; } } public class WritingController { private readonly IUserSettings userSettings; public WritingController(IUserSettings userSettings) { this.userSettings = userSettings; } public void SetTheme(string theme) { userSettings.Theme = theme; } } 複製程式碼
雖然現在ReadingController類只是用了Theme屬性的讀取器,WritingController類只使用了Theme屬性的設定器。但是由於缺乏介面分離,我們無法阻止WritingController類獲取主題資料,也無法阻止ReadingController類修改主題資料,這可是個大問題,尤其是後者。
為了防止和消除錯用介面的可能性,可以將原有介面一分為二:一個負責讀取主題資料,一個負責修改主題資料。
public interface IUserSettingsReader { string Theme { get; } } public interface IUserSettingsWriter { string Theme { set; } } 複製程式碼
UserSettingsConfig實現類現在分別實現IUserSettingsReader和IUserSettingsWriter介面
public class UserSettingsConfig : IUserSettings
=>
public class UserSettingsConfig:IUserSettingsReader,IUserSettingsWriter
客戶端現在分別只依賴它們真正需要的介面:
public class ReadingController { private readonly IUserSettingsReader userSettings; public ReadingController(IUserSettingsReader userSettings) { this.userSettings = userSettings; } public string GetTheme() { return userSettings.Theme; } } public class WritingController { private readonly IUserSettingsWriter userSettings; public WritingController(IUserSettingsWriter userSettings) { this.userSettings = userSettings; } public void SetTheme(string theme) { userSettings.Theme = theme; } } 複製程式碼
架構需要
另一種介面分離的驅動力來自於架構設計。在非對稱架構中,例如 命令查詢責任分離模式 (讀寫分離),意圖就是指導你去做一些介面分離的動作。
資料庫(表)的設計本身是面向資料,面向集合的;而現在的主流程式語言都有面向物件的一面。面向資料(集合)和麵向物件本身就是衝突的,但是在現代系統中資料庫又是必不可少的一環。為了解決這種 阻抗失衡 ,ORM(物件關係對映)應運而生。完全隔離掉資料庫,允許我們像操作物件一樣操作資料庫。現在一般的做法是,增刪改操作使用ORM,查詢使用原生SQL。對於查詢而言,越簡單,越有效率(開發效率和執行效率)最好。
示意圖如下:

客戶端構建
介面的設計(無論是分離或是其他方式產生的)會影響實現介面的型別以及使用該介面的客戶端。如果客戶端要使用介面,就必須先以某種方式獲得介面例項。為客戶端提供介面例項的方式一定程度上取決於介面實現的數目。如果每個介面都有自己特有的實現,那麼就需要構造所有的實現的例項並提供給客戶端。如果所有介面的實現都包含在單個類中,那麼只需要構建該類的例項就能滿足客戶端的所有依賴。
多實現、多例項
假設IRead、ISave和IDelete介面都有自己的實現類,客戶端就需要同時引入這三個介面。這也是我們平常開發中最常用的一種方式,基於組合實現,需要哪個介面就引入對應的介面,類似於一種可插拔的元件式開發。
public class OrderController { private readonly IRead<Order> reader; private readonly ISave<Order> saver; private readonly IDelete<Order> deleter; public OrderController(IRead<Order> reader, ISave<Order> saver, IDelete<Order> deleter) { this.reader = reader; this.saver = saver; this.deleter = deleter; } public void CreateOrder(Order order) { saver.Save(order); } public Order GetOrder(Guid orderID) { return reader.ReadOne(orderID); } public void UpdateOrder(Order order) { saver.Save(order); } public void DeleteOrder(Order order) { deleter.Delete(order); } } 複製程式碼
單實現、單例項
此種方式是在 單個類 中繼承並實現多個分離的介面,看上去也許有些反常(介面的分離的目的不是再次把它們統一在單個實現中)。常用於介面的葉子實現類,也就是說,既不是修飾器也不是介面卡的實現類,而是完成工作的實現類。在葉子實現類上應用這種方式,是因為 葉子類中所有實現的上下文是一致的 。這種方式經常應用在和Entity Framework等持久化框架直接打交道的類。
public class CreateReadUpdateDelete<TEntity>: IRead<TEntity>,ISave<TEntity>,IDelete<TEntity> { public void Save(TEntity entity) { } public IEnumerable<TEntity> ReadAll() { return new List<TEntity>(); } public void Delete(TEntity entity) { } } public OrderController CreateSingleService() { var crud = new CreateReadUpdateDelete<Order>(); return new OrderController(crud,crud,crud); } 複製程式碼
超級介面反模式
把所有介面分離得來的介面又聚合在同一個介面下是一個 常見的錯誤 ,這些介面一起聚合構成了一個“超級介面”,這破壞了介面分離帶來的好處。
public interface CreateReadUpdateDelete<TEntity>: IRead<TEntity>,ISave<TEntity>,IDelete<TEntity> { } 複製程式碼
總結
介面分離,無論是用來輔助修飾,還是為客戶端隱藏它們不應該看到的功能,還是作為架構設計的產物。我們都應該在建立任何介面時牢記介面分離這個技術原則,而且最好是從一開始就應用介面分離原則。