Loading

敏捷开发-SOLID-接口分离原则

接口所表达的是客户端代码需求和需求具体实现之间的边界。接口分离原则主张接口应该足够小。

接口的每个成员(属性、事件和方法)都需要按照接口的整体目标来实现。

除非接口的所有客户端都需要所有成员,否则要求每个实现都满足一个大而全的契约是毫无意义的。

要牢记单一职责原则和可以轻易使用的修饰器模式,对于接口包含的每个成员而言,若要实现为修饰器,必须要有一个对应的有效类比。

最简单的接口只有一个服务于单个目的的方法。这种粒度的接口看起来很像是委托,但是它们要比委托强大很多。

一个分离接口的示例

一个简单的CRUD 接口

下面这个接口本身很简单,只有五个方法。它用于允许用户对实体对象的持久存储进行CRUD操作。

CRUD代表创建、读取、更新和删除(create, read, update, and delete)。这四个动作是客户端维护实体对象持久存储的最常见的一组操作。

UML类图给出了ICreateReadUpdateDelete接口上的可用操作。
image

读取操作被分割成为两个方法,一个用于从存储中获取单个记录,另外一个用于一次获取所有记录。

一个用于对实体对象做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<TEntity>实例。

CRUD中的每个操作都是由对应的ICreateReadUPdateDelete接口实现来执行,也包括所有修饰器实现

展示的日志和事务处理等修饰器实现都是可以接受的。有些修饰器作用于所有方法

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("Creating entity of type {0}", typeof(TEntity).Name);
        decoratedCrud.Create(entity);
    }
    public TEntity ReadOne(Guid identity)
    {
        log.InfoFormat("Reading entity of type {0} with identity {1}",
                       typeof(TEntity).Name, identity);
        return decoratedCrud.ReadOne(identity);
    }
    public IEnumerable<TEntity> ReadAll()
    {
        log.InfoFormat("Reading all entities of type {0}", typeof(TEntity).Name);
        return decoratedCrud.ReadAll();
    }
    public void Update(TEntity entity)
    {
        log.InfoFormat("Updating entity of type {0}", typeof(TEntity).Name);
        decoratedCrud.Update(entity);
    }
    public void Delete(TEntity entity)
    {
        log.InfoFormat("Deleting entity of type {0}", typeof(TEntity).Name);
        decoratedCrud.Delete(entity);
    }
}
// . . .
public class CrudTransactional<TEntity> : ICreateReadUpdateDelete<TEntity>
{
    private readonly ICreateReadUpdateDelete<TEntity> decoratedCrud;
    public CrudTransactional(ICreateReadUpdateDelete<TEntity> decoratedCrud)
    {
        this.decoratedCrud = decoratedCrud;
    }
    public void Create(TEntity entity)
    {
        using (var transaction = new TransactionScope())
        {
            decoratedCrud.Create(entity);
            transaction.Complete();
        }
    }
    public TEntity ReadOne(Guid identity)
    {
        TEntity entity;
        using (var transaction = new TransactionScope())
        {
            entity = decoratedCrud.ReadOne(identity);
            transaction.Complete();
        }
        return entity;
    }
    public IEnumerable<TEntity> ReadAll()
    {
        IEnumerable<TEntity> allEntities;
        using (var transaction = new TransactionScope())
        {
            allEntities = decoratedCrud.ReadAll();
            transaction.Complete();
        }
        return allEntities;
    }
    public void Update(TEntity entity)
    {
        using (var transaction = new TransactionScope())
        {
            decoratedCrud.Update(entity);
            transaction.Complete();
        }
    }
    public void Delete(TEntity entity)
    {
        using (var transaction = new TransactionScope())
        {
            decoratedCrud.Delete(entity);
            transaction.Complete();
        }
    }
}

用于记录日志和管理事务的修饰器都属于横切关注点(cross-cutting concern)。

几乎所有的接口以及接口中的方法都可以应用日志和事务管理这两个修饰器。因此,为了避免在多个接口中重复实现,你可以使用面向方面编程来修饰接口的所有实现。

有些修饰器只应用于接口的部分方法上,而不是所有的方法。比如,你也许想要在从持久存储中永久删除某个实体前提示用户,这也是一个很常见的需求。切记,请不要去改变现有的类实现,这会违背开放与封闭原则。相反,你应该创建客户端用来执行删除动作接口的一个新实现

展示了ICreateReadUpdateDelete<TEntity>接口的Delete方法。在只要求修饰部分接口时可以使用接口分离

public class DeleteConfirmation<TEntity> : ICrud<TEntity>
{
    private readonly ICreateReadUpdateDelete<TEntity> decoratedCrud;
    public DeleteConfirmation(ICreateReadUpdateDelete<TEntity> decoratedCrud)
    {
        this.decoratedCrud = decoratedCrud;
    }
    public void Create(TEntity entity)
    {
        decoratedCrud.Create(entity);
    }
    public TEntity ReadOne(Guid identity)
    {
        return decoratedCrud.ReadOne(identity);
    }
    public IEnumerable<TEntity> ReadAll()
    {
        return decoratedCrud.ReadAll();
    }
    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);
        }
    }
}

顾名思义,DeleteConfirmation<TEntity>类只修饰了Delete方法。其余方法都是直托方法。直托(pass-through)代表对该方法不做任何实际的修饰:不修饰接口方法,就好像直接调用被修饰的接口方法一样。尽管实际上这些直托方法什么都没有做,为了确保单元测试的覆盖率并
确认它们是否正确委托,依然需要为这些直托方法编写测试方法以验证方法行为是否正确。但这样做与接口分离的方式比较起来要麻烦很多。

通过把Delete方法从ICreateReadUpdateDelete<TEntity>接口分离后,你会得到以下两个接口

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);
}

ICreateReadUpdateDelete接口一分为二后,就可以只对IDelete<TEntity>接口提供确认修饰器的实现

只在相关的接口上应用确认修饰器

public class DeleteConfirmation<TEntity> : IDelete<TEntity>
{
    private readonly IDelete<TEntity> decoratedDelete;
    public DeleteConfirmation(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);
        }
    }
}

重构原有DeleteConfirmation类的具体理由有两个:该类委托的接口已经发生了改变,以及会有不同的获得用户答复的交互方式。要求用户确认是否真的想要删除某个实体只需要一个非常简单的类似于谓词的接口.

一个很简单的接口,用于征求用户对某些事情的确认

public interface IUserInteraction
{
    bool Confirm(string message);
}

缓存

实现的修饰器是针对ReadOneReadAll这两个读取方法的。你想在这两个读取方法中缓存读取的数据并用作后续请求的返回。而对于CreateUpdate方法而言,缓存都是没有意义的.

缓存修饰器包含了冗余的直托方法

public class CrudCaching<TEntity> : ICreateReadUpdate<TEntity>
{
    private TEntity cachedEntity;
    private IEnumerable<TEntity> allCachedEntities;
    private readonly ICreateReadUpdate<TEntity> decorated;
    public CrudCaching(ICreateReadUpdate<TEntity> decorated)
    {
        this.decorated = decorated;
    }
    public void Create(TEntity entity)
    {
        decorated.Create(entity);
    }
    public TEntity ReadOne(Guid identity)
    {
        if(cachedEntity == null)
        {
            cachedEntity = decorated.ReadOne(identity);
        }
        return cachedEntity;
    }
    public IEnumerable<TEntity> ReadAll()
    {
        if (allCachedEntities == null)
        {
            allCachedEntities = decorated.ReadAll();
        }
        return allCachedEntities;
    }
    public void Update(TEntity entity)
    {
        decorated.Update(entity);
    }
}

通过再次应用接口分离原则,你可以把两个用于读取数据的方法组织到它们自己的接口中,然后就可以单独修饰这个只用于读取数据的接口。

ReadCaching类只用于修饰IRead接口

public interface IRead<TEntity>
{
    TEntity ReadOne(Guid identity);
    IEnumerable<TEntity> ReadAll();
}
// . . .
public class ReadCaching<TEntity> : IRead<TEntity>
{
    private TEntity cachedEntity;
    private IEnumerable<TEntity> allCachedEntities;
    private readonly IRead<TEntity> decorated;
    public ReadCaching(IRead<TEntity> decorated)
    {
        this.decorated = decorated;
    }
    public TEntity ReadOne(Guid identity)
    {
        if(cachedEntity == null)
        {
            cachedEntity = decorated.ReadOne(identity);
        }
        return cachedEntity;
    }
    public IEnumerable<TEntity> ReadAll()
    {
        if (allCachedEntities == null)
        {
            allCachedEntities = decorated.ReadAll();
        }
        return allCachedEntities;
    }
}

剩余的两个方法也可以统一为单个方法

public interface ICreateUpdate<TEntity>
{
    void Create(TEntity entity);
    void Update(TEntity entity);
}

除了方法名,Create和Update方法的签名完全相同。除此之外,它们的目的也很相似:前者用于保存一个新建的实体,后者用于保存一个已有的实体。你可以把这二者统一为单个Save方法,该方法内部清楚如何处理新实体和已有实体,而客户端代码不需要知道这些细节。毕竟,客户端
很有可能需要同时保存和更新某个实体,如果有了单个可用的接口时就无需两个目的类似的接口了,因为接口的客户端想要做的只是持久化指定的实体。

ISave接口的实现会创建新的实体,也会适当地更新已经存在的实体

public interface ISave<TEntity>
{
    void Save(TEntity entity);
}

经过这次重构后,你就可以针对该接口添加新的审计修饰器了。每当用户保存一个实体,你都想要增加一些元数据到持久存储中。特别是有关触发保存动作的用户身份和时间信息。

审计修饰器内部使用了两个ISave接口

public class SaveAuditing<TEntity> : ISave<TEntity>
{
    private readonly ISave<TEntity> decorated;
    private readonly ISave<AuditInfo> auditSave;
    public SaveAuditing(ISave<TEntity> decorated, ISave<AuditInfo> auditSave)
    {
        this.decorated = decorated;
        this.auditSave = auditSave;
    }
    public void Save(TEntity entity)
    {
        decorated.Save(entity);
        var auditInfo = new AuditInfo
        {
            UserName = Thread.CurrentPrincipal.Identity.Name,
            TimeStamp = DateTime.Now
        };
        auditSave.Save(auditInfo);
    }
}

SaveAuditing修饰器本身实现了ISave接口,但也需要两个不同的ISave接口实现来构造修饰器本身。

第一个必须是符合修饰器的TEntity泛型类型参数的接口实现,用来完成真正的保存工作.

第二个接口实现只针对要保存的AuditInfo类型。虽然上面代码清单中并没有展示AuditInfo类型的定义,但是可以推断它应该包含string类型的UserName属性和DateTime类型的Timestamp属性。

当客户端调用Save方法时,会创建一个新的AuditInfo实例并对其属性进行设置。在保存实体数据后,会紧跟着保存该AuditInfo实例包含的新记录数据。

同样,客户端代码应该不清楚这些细节;审计动作对于用户来说是不可察觉的,同时也不会影响到实体数据保存的实现。相似地,ISave<TEntity>接口的叶子实现(也就是用来完成实际保存工作的,没有任何修饰的实现)本身既不知道其他相关修饰器的存在,也不需要为任何具体的修饰做改动。

至此,原来的单个接口已经变为了三个独立的接口,同时,每个接口也都有了自己特有的、有意义的、具有实际功能的修饰器。下图展示的UML类图包括了分离原有接口产生的三个新接口和相应的修饰器。

接口分离能让你针对必要的方法做修饰,并且不会产生冗余
image

多重接口修饰

为了在保存或删除记录时发布一个事件通知。这个通知允许不同的订阅者根据持久存储上的变更做出不同的响应。

注意,这个事件通知对于读取数据而言是没有多大用处的,因此这里不会针对IRead接口进行修饰。

为了达到这个目的,你首先需要一个发布和订阅事件通知的机制。

分别用于发布和订阅事件通知的两个接口

public interface IEventPublisher
{
    void Publish<TEvent>(TEvent @event)
        where TEvent : IEvent;
}
// . . .
public interface IEventSubscriber
{
    void Subscribe<TEvent>(TEvent @event)
        where TEvent : IEvent;
}

示例中的IEvent接口非常简单,只包含一个string类型的Name属性.

这个修饰器会在实体被删除时发布一个事件通知

public class DeleteEventPublishing<TEntity> : IDelete<TEntity>
{
    private readonly IDelete<TEntity> decorated;
    private readonly IEventPublisher eventPublisher;
    public DeleteEventPublishing(IDelete<TEntity> decorated, IEventPublisher
                                 eventPublisher)
    {
        this.decorated = decorated;
        this.eventPublisher = eventPublisher;
    }
    public void Delete(TEntity entity)
    {
        decorated.Delete(entity);
        var entityDeleted = new EntityDeletedEvent<TEntity>(entity);
        eventPublisher.Publish(entityDeleted);
    }
}

你有两个选择:要么在单个修饰器类中同时支持IDelete和ISave两个接口,要么再为ISave接口单独实现一个发布事件通知的修饰器。

第一个选项的实现,它通过增加新的Save方法来让已有的修饰器也支持ISave接口。两个修饰器可以在一个类中实现

public class ModificationEventPublishing<TEntity> : IDelete<TEntity>, ISave<TEntity>
{
    private readonly IDelete<TEntity> decoratedDelete;
    private readonly ISave<TEntity> decoratedSave;
    private readonly IEventPublisher eventPublisher;
    public ModificationEventPublishing(IDelete<TEntity> decoratedDelete, ISave<TEntity>
                                       decoratedSave, IEventPublisher eventPublisher)
    {
        this.decoratedDelete = decoratedDelete;
        this.decoratedSave = decoratedSave;
        this.eventPublisher = eventPublisher;
    }
    public void Delete(TEntity entity)
    {
        decoratedDelete.Delete(entity);
        var entityDeleted = new EntityDeletedEvent<TEntity>(entity);
        eventPublisher.Publish(entityDeleted);
    }
    public void Save(TEntity entity)
    {
        decoratedSave.Save(entity);
        var entitySaved = new EntitySavedEvent<TEntity>(entity);
        eventPublisher.Publish(entitySaved);
    }
}

如上面示例所示,只有在多个修饰器共享上下文信息时,在单个类中包含多个修饰器的实现才是有意义的。ModificationEventPublishing修饰器为它实现的ISaveIDelete两个接口实现相同的发布事件通知的功能。

但是,将事件发布和审计两种不同目的的修饰器结合在同一个类实现中则是不可取的。因为这样会产生不必要的依赖关系, 其中的一个修饰器依赖IEventPublisher接口,而另一个则依赖AuditInfo类型。最好还是把这些不同功能的实现和相应的依赖链分别封装在它们各自的程序集中。

客户端构建

为客户端提供接口实例的方式一定程度上取决于接口实现的数目。如果每个接口都有自己特有的实现,那就需要构造所有实现的实例并提供给客户端。或者,如果所有接口的实现都包含在单个类中,那么只需要构建该类的实例就能够满足客户端的所有依赖。

多实现、多实例

该特定于订单的控制器要求CRUD的每个方面作为一个单独的依赖项

public class OrderController
{
    private readonly IRead<Order> reader;
    private readonly ISave<Order> saver;
    private readonly IDelete<Order> deleter;
    public OrderController(IRead<Order> orderReader,
                           ISave<Order> orderSaver,
                           IDelete<Order> orderDeleter)
    {
        reader = orderReader;
        saver = orderSaver;
        deleter = orderDeleter;
    }
    public void CreateOrder(Order order)
    {
        saver.Save(order);
    }
    public Order GetSingleOrder(Guid identity)
    {
        return reader.ReadOne(identity);
    }
    public void UpdateOrder(Order order)
    {
        saver.Save(order);
    }
    public void DeleteOrder(Order order)
    {
        deleter.Delete(order);
    }
}

示例中的控制器是专门针对订单实体的。这就意味着提供的每个接口都要以Order类作为泛型参数。如果你要在任意一个接口中使用另外一种类型,那么该接口提供的操作也会需要同一种类型。比如,如果你决定将删除接口参数更改为IDelete<Customer>,那么OrderController
DeleteOrder方法就会报错,因为你在尝试用只接受Customers的方法删除一个Order。这就是强类型和泛型在同时起作用。

控制器类中的每个方法都需要一个不同的接口来执行它的功能。为了清晰起见,每个方法都直接调用相应接口的对应方法。当然,实际情况很可能不是这样的.

针对实体类型的泛型化控制器类需要每个CRUD接口的泛型参数也都是实体类型

public class GenericController<TEntity>
{
    private readonly IRead<TEntity> reader;
    private readonly ISave<TEntity> saver;
    private readonly IDelete<TEntity> deleter;
    public GenericController(IRead<TEntity> entityReader, ISave<TEntity> entitySaver,
                             IDelete<TEntity> entityDeleter)
    {
        reader = entityReader;
        saver = entitySaver;
        deleter = entityDeleter;
    }
    public void CreateEntity(TEntity entity)
    {
        saver.Save(entity);
    }
    public TEntity GetSingleEntity(Guid identity)
    {
        return reader.ReadOne(identity);
    }
    public void UpdateEntity(TEntity entity)
    {
        saver.Save(entity);
    }
    public void DeleteEntity(TEntity entity)
    {
        deleter.Delete(entity);
    }
}

使用依赖的不同实例来创建OrderController

static OrderController CreateSeparateServices()
{
    var reader = new Reader<Order>();
    var saver = new Saver<Order>();
    var deleter = new Deleter<Order>();
    return new OrderController(reader, saver, deleter);
}

为分离得到的接口分别创建不同的实现类,实际上也就分离了实现。OrderController类的关键点是,它的三个参数(readersaverdelete)不仅仅是代表三种接口,也代表了三种实现类。

单实现、单实例

另外一种实现多个分离的接口的方式是在单个类中继承并实现它们

所有接口可以在一个类中实现

public class CreateReadUpdateDelete<TEntity> :
IRead<TEntity>, ISave<TEntity>, IDelete<TEntity>
{
    public TEntity ReadOne(Guid identity)
    {
        return default(TEntity);
    }
    public IEnumerable<TEntity> ReadAll()
    {
        return new List<TEntity>();
    }
    public void Save(TEntity entity)
    {
    }
    public void Delete(TEntity entity)
    {
    }
}

切记,客户端根本不知道有这个类存在。在编译时,客户端代码只知道它所需要的一个个接口。

不论底层接口的实现是否还有其他可用的操作,对于客户端而言,每个接口都只包括接口声明的成员。这也是接口封装和隐藏信息的方式。

接口就像是实现类上的小窗口,对客户端屏蔽了他们不该看到的部分。

唯一需要改动的就是为要构造的控制器提供参数的方式

尽管下面的示例看起来不太正常,但这的确是接口分离已知的副作用之一

public OrderController CreateSingleService()
{
    var crud = new CreateReadUpdateDelete<Order>();
    return new OrderController(crud, crud, crud);
}

超级接口反模式

把所有接口分离得来的接口又聚合在同一个接口下是对接口分离原则的错误规避

interface IInterfaceSoupAntiPattern<TEntity> 
    : IRead<TEntity>, ISave<TEntity>,IDelete<TEntity>
{
}

这些接口一起聚合构成了一个“超级接口”,但这显然破坏了接口分离带来的好处。这种超级接口的实现者必然需要再次提供所有操作的实现,而这样做又会把各个修饰器的目标混合在一起。

接口分离

应用接口分离的另外两个原因分别是客户端架构的需要

客户端需要

切记,客户端只需要它们需要的东西。那些巨型接口倾向于给用户提供更多的控制能力。带有大量成员的接口允许客户端做很多操作,甚至包括它们不应该做的,这样的接口意图很模糊,焦点不明确。所有的类应该总是只有单个清晰的职责。

用户配置接口允许访问程序当前的主题

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.AppSettings.Settings[ThemeSetting].Value;
        }
        set
        {
            config.AppSettings.Settings[ThemeSetting].Value = value;
            config.Save();
            ConfigurationManager.RefreshSection("appSettings");
        }
    }
}

接口的不同客户端以不同的目的使用同一个属性

public class ReadingController
{
    private readonly IUserSettings settings;
    public ReadingController(IUserSettings settings)
    {
        this.settings = settings;
    }
    public string GetTheme()
    {
        return settings.Theme;
    }
}
// . . .
public class WritingController
{
    private readonly IUserSettings settings;
    public WritingController(IUserSettings settings)
    {
        this.settings = settings;
    }
    public void SetTheme(string theme)
    {
        settings.Theme = theme;
    }
}

原有接口被一分为二:一个负责读取主题数据,另外一个负责修改

public interface IUserSettingsReader
{
    string Theme
    {
        get;
    }
}
// . . .
public interface IUserSettingsWriter
{
    string Theme
    {
        set;
    }
}

这两个接口现在分别只依赖它们真正需要的接口

public class ReadingController
{
    private readonly IUserSettingsReader settings;
    public ReadingController(IUserSettingsReader settings)
    {
        this.settings = settings;
    }
    public string GetTheme()
    {
        return settings.Theme;
    }
}
// . . .
public class WritingController
{
    private readonly IUserSettingsWriter settings;
    public WritingController(IUserSettingsWriter settings)
    {
        this.settings = settings;
    }
    public void SetTheme(string theme)
    {
        settings.Theme = theme;
    }
}

通过接口分离,你已经防止了ReadingController类对用户设置的修改,也防止了Writing-Controller类对用户设置的读取。因此,开发人员也不会无意地错用控制器来完成它们不应该支持的操作。

UsersSettingsConfig类现在同时实现了两个接口,但是使用这些接口的客户端对此并不知情

public class UserSettingsConfig : IUserSettingsReader, IUserSettingsWriter
{
    private const string ThemeSetting = "Theme";
    private readonly Configuration config;
    public UserSettingsConfig()
    {
        config = ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.None);
    }
    public string Theme
    {
        get
        {
            return config.AppSettings.Settings[ThemeSetting].Value;
        }
        set
        {
            config.AppSettings.Settings[ThemeSetting].Value = value;
            config.Save();
            ConfigurationManager.RefreshSection("appSettings");
        }
    }
}

通过使用方法代替属性,现在的IUserSettingWriter接口可以继承IUserSettingReader接口

public interface IUserSettingsReader
{
    string GetTheme();
}
// . . .
public interface IUserSettingsWriter : IUserSettingsReader
{
    void SetTheme(string theme);
}

WritingController类通过一个接口就能同时访问读取器和设置器

public class WritingController
{
    private readonly IUserSettingsWriter settings;
    public WritingController(IUserSettingsWriter settings)
    {
        this.settings = settings;
    }
    public void SetTheme(string theme)
    {
        if (settings.GetTheme() != theme)
        {
            settings.SetTheme(theme);
        }
    }
}

授权

该接口只包含匿名用户可以执行的操作

public interface IUnauthorized
{
    IAuthorized Login(string username, string password);
    void RequestPasswordReminder(string emailAddress);
}

登录后,用户将可以访问已授权的操作

public interface IAuthorized
{
    void ChangePassword(string oldPassword, string newPassword);
    void AddToBasket(Guid itemID);
    void Checkout();
    void Logout();
}

架构需要

另外一种接口分离的驱动力来自于架构设计。高层设计产生的决定对底层代码的组织有着非常大的影响。

IPersistence接口既包括命令,也包括查询

public interface IPersistence
{
    IEnumerable<Item> GetAll();
    Item GetByID(Guid identity);
    IEnumerable<Item> FindByCriteria(string criteria);
    void Save(Item item);
    void Delete(Item item);
}

该接口的非对称架构就是命令查询责任分离模式的一部分。这里再次出现分离(segregation)这个词并不是巧合,因为这个架构模式本身的意图就是指导你去做一些接口分离的动作。

当命令和查询的处理非对称时,实现一片混乱

public class Persistence : IPersistence
{
    private readonly ISession session;
    private readonly MongoDatabase mongo;
    public Persistence(ISession session, MongoDatabase mongo)
    {
        this.session = session;
        this.mongo = mongo;
    }
    public IEnumerable<Item> GetAll()
    {
        return mongo.GetCollection<Item>("items").FindAll();
    }
    public Item GetByID(Guid identity)
    {
        return mongo.GetCollection<Item>("items").FindOneById(identity.ToBson());
    }
    public IEnumerable<Item> FindByCriteria(string criteria)
    {
        var query = BsonSerializer.Deserialize<QueryDocument>(criteria);
        return mongo.GetCollection<Item>("Items").Find(query);
    }
    public void Save(Item item)
    {
        using(var transaction = session.BeginTransaction())
        {
            session.Save(item);
            transaction.Commit();
        }
    }
    public void Delete(Item item)
    {
        using(var transaction = session.BeginTransaction())
        {
            session.Delete(item);
            transaction.Commit();
        }
    }
}
根据架构需要分离接口能让各个实现带有自己必需的依赖
image

理想情况下,这两个接口的实现不仅仅是不同的类,而且也会依赖不同的包(程序集)。如果依然有着相同的依赖,仅仅通过接口分离只能解决部分问题。因为它们的实现和各自的依赖链是相互关联的,所以无法单独重用其中的某个实现.

接口分离为了查询和命令方法

public interface IPersistenceQueries
{
    IEnumerable<Item> GetAll();
    Item GetByID(Guid identity);
    IEnumerable<Item> FindByCriteria(string criteria);
}
// . . .
public interface IPersistenceCommands
{
    void Save(Item item);
    void Delete(Item item);
}

查询实现仅依赖MongoDB

public class PersistenceQueries : IPersistenceQueries
{
    private readonly MongoDatabase mongo;
    public Persistence(MongoDatabase mongo)
    {
        this.mongo = mongo;
    }
    public IEnumerable<Item> GetAll()
    {
        return mongo.GetCollection<Item>("items").FindAll();
    }
    public Item GetByID(Guid identity)
    {
        return mongo.GetCollection<Item>("items").FindOneById(identity.ToBson());
    }
    public IEnumerable<Item> FindByCriteria(string criteria)
    {
        var query = BsonSerializer.Deserialize<QueryDocument>(criteria);
        return mongo.GetCollection<Item>("Items").Find(query);
    }
}

命令实现仅依赖NHibernate

public class PersistenceCommands : IPersistenceCommands
{
    private readonly ISession session;
    public PersistenceCommands(ISession session)
    {
        this.session = session;
    }
    public void Save(Item item)
    {
        using(var transaction = session.BeginTransaction())
        {
            session.Save(item);
            transaction.Commit();
        }
    }
    public void Delete(Item item)
    {
        using(var transaction = session.BeginTransaction())
        {
            session.Delete(item);
            transaction.Commit();
        }
    }
}

单方法接口

接口分离会生成很小的接口。接口规模越小,就变得越通用。

posted @ 2022-04-30 10:31  F(x)_King  阅读(80)  评论(0编辑  收藏  举报