界面隔离和单一责任原则困扰

时间:2015-09-18 17:32:41

标签: c# interface solid-principles single-responsibility-principle interface-segregation-principle

我试图遵循界面隔离单一责任原则,但我对如何将它们整合在一起感到困惑。< / p>

这里我举了几个接口的例子,我将它分成更小,更直接的接口:

public interface IDataRead
{
    TModel Get<TModel>(int id);
}

public interface IDataWrite
{
    void Save<TModel>(TModel model);
}

public interface IDataDelete
{        
    void Delete<TModel>(int id);
    void Delete<TModel>(TModel model);
}

我略微简化了(有一些where条款妨碍了可读性。)

目前我正在使用 SQLite ,但是,这种模式的优点在于,如果我选择不同的数据存储方法,如 Azure ,例如。

现在,我对每个接口都有一个实现,这里是每个接口的简化示例:

public class DataDeleterSQLite : IDataDelete
{
    SQLiteConnection _Connection;

    public DataDeleterSQLite(SQLiteConnection connection) { ... }

    public void Delete<TModel>(TModel model) { ... }
}

... 

public class DataReaderSQLite : IDataRead
{
    SQLiteConnection _Connection;

    public DataReaderSQLite(SQLiteConnection connection) { ... }

    public TModel Get<TModel>(int id) { ... }
}

// You get the idea.

现在,我遇到了将它们整合在一起的问题,我确定一般的想法是创建一个使用接口Database类,而不是类(真正的实现)。所以,我想出了类似的东西:

public class Database
{
    IDataDelete _Deleter;
    ...

    //Injecting the interfaces to make use of Dependency Injection.
    public Database(IDataRead reader, IDataWrite writer, IDataDelete deleter) { ... }
}

这里的问题是我应该如何向客户端公开IDataReadIDataWriteIDataDelete接口?我应该重写方法重定向到接口吗?像这样:

//This feels like I'm just repeating a load of work.
public void Delete<TModel>(TModel model)
{
    _Deleter.Delete<TModel>(model);
}

突出显示我的评论,这看起来有点愚蠢,我把这些类分成很好的,分开的实现很麻烦,现在我把它们全部重新组合在一个超级类中。

我可以将接口公开为属性,如下所示:

public IDataDelete Deleter { get; private set; }

这感觉好一点,然而,客户不应该经历决定他们需要使用哪个界面的麻烦。

我完全忽略了这一点吗?救命啊!

5 个答案:

答案 0 :(得分:3)

  

我完全忽略了这一点吗?救命啊!

我认为你并没有完全错过它,你是在正确的轨道上,但在这种情况下走得太远了。您的所有CRUD功能都完全相互关联,因此它们属于公开单一职责的单一界面。如果你的界面暴露了CRUD函数和其他一些责任,那么在我看来,重构成单独的接口是一个很好的选择。

如果作为您的功能的消费者,我必须为插入,删除等实例化不同的类,我会来找你。

答案 1 :(得分:2)

不是一个真正的答案,但我想在这里放更多,而不是评论允许。感觉您正在使用存储库模式,因此您可以使用IRepository将其全部包装起来。

interface IRepository
{
    T Get<TModel>(int id);
    T Save<TModel>(TModel model);
    void Delete<TModel>(TModel model);
    void Delete<TModel>(int id);
}

现在你可以像上面一样拥有一个具体的数据库:

class Database : IRepository
{
    private readonly IDataReader _reader;
    private readonly IDataWriter _writer;
    private readonly IDataDeleter _deleter;

    public Database(IDataReader reader, IDataWriter writer, IDataDeleter deleter)
    {
        _reader = reader;
        _writer = writer;
        _deleter = deleter;
    }

    public T Get<TModel>(int id) { _reader.Get<TModel>(id); }

    public T Save<TModel>(TModel model) { _writer.Save<TModel>(model); }

    public void Delete<TModel>(TModel model) { _deleter.Delete<TModel>(model); }

    public void Delete<TModel>(int id) { _deleter.Delete<TModel>(id); }
}

是的,从表面上看,它看起来像是一种不必要的抽象,但有很多好处。正如@moarboilerplate所说,是他的回答,不要让#34;最好&#34;实践阻碍了产品的交付。您的产品决定了您需要遵循的原则和产品所需的抽象级别。

以上是采用上述方法的一个快速好处:

class CompositeWriter : IDataWriter
{
    public List<IDataWriter> Writers { get; set; }

    public void Save<TModel>(model)
    {
        this.Writers.ForEach(writer =>
        {
            writer.Save<TModel>(model);
        });
    }
}

class Database : IRepository
{
    private readonly IDataReader _reader;
    private readonly IDataWriter _writer;
    private readonly IDataDeleter _deleter;
    private readonly ILogger _logger;

    public Database(IDataReader reader, IDataWriter writer, IDataDeleter deleter, ILogger _logger)
    {
        _reader = reader;
        _writer = writer;
        _deleter = deleter;
        _logger = logger;
    }

    public T Get<TModel>(int id)
    {
        var sw = Stopwatch.StartNew();

        _writer.Get<TModel>(id);

        sw.Stop();

        _logger.Info("Get Time: " + sw. ElapsedMilliseconds);
    }

    public T Save<TModel>(TModel model)
    {
         //this will execute the Save method for every writer in the CompositeWriter
         _writer.Save<TModel>(model);
    }

    ... other methods omitted
}

现在你可以有地方来增强功能。上面的示例显示了如何使用不同的IDataReader并为它们计时,而无需在每个IDataReader中添加日志记录和计时。这也显示了如何使用可以将数据实际存储到多个商店的复合IDataWriter。

所以,是的,抽象确实带来了一些管道,它可能感觉好像不需要它,但是根据你项目的生命周期,这可以为你节省大量的技术债务。将来

答案 2 :(得分:2)

  

这里的问题是我应该如何向客户端公开IDataRead,IDataWrite和IDataDelete接口?

如果您创建这些接口,则已将它们暴露给客户端。客户端可以将其用作使用Dependency Injection注入使用类的依赖项。

  

我很难将这些类分成很好的,分开的实现,现在我将它们全部重新组合在一个巨型课程中。

ISP是关于分离接口而非实现。在您的事业中,您甚至可以在一个类中实现这些接口,因为您在实现中实现了高内聚。客户甚至不知道您在一个类中实现这些接口。

public class Database : IDataRead, IDataWrite, IDataDelete
{
}

这可能类似于以下内容:

public interface IRepository : IDataRead, IDataWrite, IDataDelete
{
}

但是,你不应该这样做,因为你失去了坚持ISP的优势。您分离了接口并创建了另一个聚合其他接口的接口。因此,每个使用IRepository接口的客户端仍然被迫实现所有接口。这有时称为interface soup anti-pattern

  然而,客户不应该经历决定他们需要使用哪个界面的麻烦。

实际上,我觉得你错过了这里的观点。客户必须知道他想做什么,以及ISP告诉我们客户不应该被迫使用他不需要的方法。

在您展示的示例中,当您关注ISP时,很容易创建不对称数据访问。这是CQRS架构中常见的概念。想象一下,您希望将读取与写入分开。实现这一目标实际上您不需要修改现有代码(因为您也遵守OCP)。您需要做的是提供IDataRead接口的新实现,并在Dependency Injection容器中注册此实现

答案 3 :(得分:1)

当我们讨论界面隔离(甚至是单一责任)时,我们讨论的是制作一组实体,这些实体在逻辑上相关并组合在一起形成一个有意义的完整实体。

这个想法是,一个类应该能够从数据库中读取一个实体,并用新值更新它。但是,一个班级不应该能够获得罗马的天气并更新纽约证券交易所的股票价值!

为读,写,删除创建单独的接口有点极端。互联网服务提供商实际上没有强加规则来在接口中放置一个操作。理想情况下,可以读取,写入,删除的接口可以完成(但不具有相关操作的笨重)接口。在这里,界面中的操作应该是related而不是dependent

因此,传统上,您可以拥有类似

的界面
interface IRepository<T>
{
    IEnumerable<T> Read();
    T Read(int id);
    IEnumerable<T> Query(Func<T, bool> predicate);
    bool Save(T data);
    bool Delete(T data);
    bool Delete(int id);
}

您可以将此界面传递给客户端代码,这对他们来说非常有意义。并且它可以与遵循一组基本规则的任何类型的实体一起工作(例如,每个实体应该由整数id唯一地标识)。

此外,如果您的业务/应用程序层类仅依赖于此接口,而不是实际的实现类,如此

class EmployeeService
{
    readonly IRepository<Employee> _employeeRepo;

    Employee GetEmployeeById(int id)
    {
        return _employeeRepo.Read(id);
    }

    //other CRUD operation on employee
}

然后,您的业务/应用程序类将完全独立于数据存储基础结构。您可以灵活地选择自己喜欢的数据存储,只需将其插入到代码库中即可实现此接口。

您可以OracleRepository : IRepository和/或MongoRepository : IRepository并在需要时通过IoC注入正确的一个。

答案 4 :(得分:1)

当我设计存储库时,我总是在考虑阅读写作

这意味着我目前正在使用这些接口:

/// <summary>
/// Inform an underlying data store to return a set of read-only entity instances.
/// </summary>
/// <typeparam name="TEntity">The entity type to return read-only entity instances of.</typeparam>
public interface IEntityReader<out TEntity> where TEntity : Entity
{
    /// <summary>
    /// Inform an underlying data store to return a set of read-only entity instances.
    /// </summary>
    /// <returns>IQueryable for set of read-only TEntity instances from an underlying data store.</returns>
    IQueryable<TEntity> Query();
}

/// <summary>
/// Informs an underlying  data store to accept sets of writeable entity instances.
/// </summary>
/// <typeparam name="TEntity"></typeparam>
public interface IEntityWriter<in TEntity> where TEntity : Entity
{
    /// <summary>
    /// Inform an underlying data store to return a single writable entity instance.
    /// </summary>
    /// <param name="primaryKey">Primary key value of the entity instance that the underlying data store should return.</param>
    /// <returns>A single writable entity instance whose primary key matches the argument value(, if one exists in the underlying data store. Otherwise, null.</returns>
    TEntity Get(object primaryKey);

    /// <summary>
    /// Inform the underlying  data store that a new entity instance should be added to a set of entity instances.
    /// </summary>
    /// <param name="entity">Entity instance that should be added to the TEntity set by the underlying data store.</param>
    void Create(TEntity entity);

    /// <summary>
    /// Inform the underlying data store that an existing entity instance should be permanently removed from its set of entity instances.
    /// </summary>
    /// <param name="entity">Entity instance that should be permanently removed from the TEntity set by the underlying data store.</param>
    void Delete(TEntity entity);

    /// <summary>
    /// Inform the underlying data store that an existing entity instance's data state may have changed.
    /// </summary>
    /// <param name="entity">Entity instance whose data state may be different from that of the underlying data store.</param>
    void Update(TEntity entity);
}

/// <summary>
/// Synchronizes data state changes with an underlying data store.
/// </summary>
public interface IUnitOfWork
{
    /// <summary>
    /// Saves changes tot the underlying data store
    /// </summary>
    void SaveChanges();
}

有些人可能会说IEntityWriter有点矫枉过正,可能会违反SRP,因为它既可以创建也可以删除实体,而且IReadEntities是漏洞,因为没有人可以完全实施IQueryable<TEntity> - 但仍然无法找到完美的方式。

对于实体框架我然后实现所有这些接口:

internal sealed class EntityFrameworkRepository<TEntity> : 
    IEntityReader<TEntity>, 
    IEntityWriter<TEntity>, 
    IUnitOfWork where TEntity : Entity
{
    private readonly Func<DbContext> _contextProvider;

    public EntityFrameworkRepository(Func<DbContext> contextProvider)
    {
        _contextProvider = contextProvider;
    }

    public void Create(TEntity entity)
    {
        var context = _contextProvider();
        if (context.Entry(entity).State == EntityState.Detached)
        {
            context.Set<TEntity>().Add(entity);
        }
    }

    public void Delete(TEntity entity)
    {
        var context = _contextProvider();
        if (context.Entry(entity).State != EntityState.Deleted)
        {
            context.Set<TEntity>().Remove(entity);
        }  
    }

    public void Update(TEntity entity)
    {
        var entry = _contextProvider().Entry(entity);
        entry.State = EntityState.Modified;
    }

    public IQueryable<TEntity> Query()
    {
        return _contextProvider().Set<TEntity>().AsNoTracking();
    }

    public TEntity Get(object primaryKey)
    {
        return _contextProvider().Set<TEntity>().Find(primaryKey);
    }

    public void SaveChanges()
    {
        _contextProvider().SaveChanges();
    }
}

然后我依赖IWriteEntities<MyEntity>上的命令处理程序和IReadEntities<MyEntity>上的查询处理程序。保存实体(使用IUnitOfWork)是通过使用IoC的装饰器模式完成的。