編碼最佳實踐——接口分離原則

接口分離原則

在面向對象編程中,接口是一個很是重要的武器。接口所表達的是客戶端代碼需求和需求具體實現之間的邊界。接口分離原則主張接口應該足夠小,大而全的契約(接口)是毫無心義的。數據庫

接口分離的緣由

將大型接口分割爲多個小型接口的緣由有:編程

①須要單獨修飾接口c#

②客戶端須要微信

③架構須要架構

須要單獨修飾接口

咱們經過拆解一個單個巨型接口到多個小型接口的示例,分離過程當中建立了各類各樣的修飾器,來說解大量應用接口分離原則帶來的主要好處。app

下面這個接口包含了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)來修飾接口的全部實現。ide

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("Reading  entity of type {0}", typeof(TEntity).Name);
        return decoratedCrud.ReadOne(identity);
    }

    public void Update(TEntity entity)
    {
        log.InfoFormat("Update  entity of type {0}", typeof(TEntity).Name);
        decoratedCrud.Update(entity);
    }
}

可是有些修飾器只應用於接口的部分方法上,而不是全部的方法。假設如今有這麼一個需求,在持久化存儲中刪除某個實體前提示用戶。切記不要直接去修改現有的類實現,由於這會違背開放與封閉原則。相反,應該建立一個客戶端用來刪除實體的新實現。post

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。對於查詢而言,越簡單,越有效率(開發效率和執行效率)最好。

示意圖以下:

mark

客戶端構建

接口的設計(不管是分離或是其餘方式產生的)會影響實現接口的類型以及使用該接口的客戶端。若是客戶端要使用接口,就必須先以某種方式得到接口實例。爲客戶端提供接口實例的方式必定程度上取決於接口實現的數目。若是每一個接口都有本身特有的實現,那麼就須要構造全部的實現的實例並提供給客戶端。若是全部接口的實現都包含在單個類中,那麼只須要構建該類的實例就能知足客戶端的全部依賴。

多實現、多實例

假設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>
{
    
}

總結

接口分離,不管是用來輔助修飾,仍是爲客戶端隱藏它們不該該看到的功能,仍是做爲架構設計的產物。咱們都應該在建立任何接口時牢記接口分離這個技術原則,並且最好是從一開始就應用接口分離原則。

參考

《C#敏捷開發實踐》

做者:CoderFocus

微信公衆號:

聲明:本文爲博主學習感悟總結,水平有限,若是不當,歡迎指正。若是您認爲還不錯,不妨點擊一下下方的推薦按鈕,謝謝支持。轉載與引用請註明做者及出處。

相關文章
相關標籤/搜索