4

我正在尝试遵循接口隔离单一职责原则,但是我对如何将它们组合在一起感到困惑。

在这里,我有几个接口的示例,我将其拆分为更小、更定向的接口:

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

这感觉好一点,但是,不应该期望客户端必须经历决定他们需要使用哪个接口的麻烦。

我完全错过了这里的重点吗?帮助!

4

6 回答 6

4

通过这个例子,如果你想基于接口的组合来定义一个对象的能力,那么分解每种类型的操作的力量就很大了。

所以你可以有一些只有 Gets 的东西,还有一些 Gets、Saves 和 Deletes 的东西,还有一些只有 Saves 的东西。然后,您可以将它们传递给其方法或构造函数仅调用 ISave 或其他对象的对象。这样他们就不用担心知道某些东西是如何保存的,只需知道它是通过接口公开的 Save() 方法调用的。

或者,您可能会遇到这样一种情况:数据库实现了所有接口,但随后它被传递给只关心触发写入、读取或更新等的对象——因此当它被传递给那个时object 它作为适当的接口类型传递,并且执行其他操作的能力不会暴露给消费者。

考虑到这一点,您的应用程序很可能不需要这种类型的功能。您可能没有使用来自不同来源的数据,并且需要抽象一种在它们之间调用 CRUD 操作的通用方法,这是第一个将解决的问题,或者需要将数据库作为数据源的概念解耦,如与支持 CRUD 操作的对象相反,这是第二个要解决的问题。所以请确保使用它来满足需求,而不是试图遵循最佳实践——因为这只是采用某种实践的一种方式,但它是否“最佳”只能在上下文中确定它正在解决的问题。

于 2015-09-18T17:45:08.943 回答
3

我完全错过了这里的重点吗?帮助!

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

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

于 2015-09-18T17:51:40.177 回答
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容器中

于 2015-09-19T08:09:00.610 回答
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 所说,他的回答是,不要让“最佳”实践妨碍产品的交付。您的产品决定了您需要遵循哪些原则以及产品所需的抽象级别。

这是继续使用上述方法的一个快速好处:

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,它实际上可以将数据存储到多个存储中。

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

于 2015-09-18T18:09:06.977 回答
1

当我们谈论接口隔离(甚至是单一职责)时,我们谈论的是使实体执行一组逻辑相关的操作并组合在一起以形成一个有意义的完整实体。

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

为 Read、Write、Delete 制作单独的接口有点极端。ISP 并没有从字面上强加一个规则来在接口中只放置一个操作。理想情况下,一个可以读取、写入、删除的接口构成一个完整的(但不包含不相关操作的庞大)接口。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

于 2015-09-18T18:22:16.247 回答
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 的装饰器模式完成的。

于 2015-09-21T08:03:08.687 回答