编码最佳实践——接口分离原则

接口分离原则

在面向对象编程中,接口是一个很是重要的武器。接口所表达的是客户端代码需求和需求具体实现之间的边界。接口分离原则主张接口应该足够小,大而全的契约(接口)是毫无心义的。数据库

接口分离的缘由

将大型接口分割为多个小型接口的缘由有:编程

①须要单独修饰接口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);
    }
}
复制代码

可是有些修饰器只应用于接口的部分方法上,而不是全部的方法。假设如今有这么一个需求,在持久化存储中删除某个实体前提示用户。切记不要直接去修改现有的类实现,由于这会违背开放与封闭原则。相反,应该建立一个客户端用来删除实体的新实现。测试

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
微信公众号:

相关文章
相关标签/搜索