Несоответствие принципа разделения интерфейса и принципа единой ответственности

Я пытаюсь следовать принципам сегрегации интерфейса и единой ответственности, однако я не понимаю, как все это объединить.

Здесь у меня есть пример нескольких интерфейсов, которые я разделил на меньшие, более направленные интерфейсы:

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) { ... }
}

Вопрос здесь в том, как я должен разоблачить IDataRead, IDataWrite, а также IDataDelete интерфейсы к клиенту? Должен ли я переписать методы для перенаправления на интерфейс? Как это:

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

Это выглядит немного лучше, однако от клиента не следует ожидать, что ему придется решать, какой интерфейс ему нужно использовать.

Я полностью упускаю суть здесь? Помогите!

6 ответов

Решение

Когда мы говорим о разделении интерфейса (и даже об одной ответственности), мы говорим о создании сущностей, которые выполняют набор операций, которые логически связаны и соединяются вместе, чтобы сформировать значимую целостную сущность.

Идея в том, что класс должен иметь возможность читать сущность из базы данных и обновлять ее новыми значениями. Но класс не должен иметь возможность узнать погоду в Риме и обновить стоимость акций на NYSE!

Создание отдельных интерфейсов для чтения, записи, удаления немного экстремально. Интернет-провайдер буквально не налагает правила, чтобы поместить только одну операцию в интерфейс. В идеале интерфейс, который может читать, записывать, удалять, создает полный (но не громоздкий интерфейс с несвязанными операциями) интерфейсом. Здесь операции в интерфейсе должны быть 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);
}

Этот интерфейс вы можете передать клиентскому коду, который имеет для них полный смысл. И он может работать с любым типом объекта, который следует базовому набору правил (например, каждый объект должен быть уникально идентифицирован целочисленным идентификатором).

Кроме того, если ваши классы уровня Business/Application зависят только от этого интерфейса, а не от фактического реализующего класса, как это

class EmployeeService
{
    readonly IRepository<Employee> _employeeRepo;

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

    //other CRUD operation on employee
}

Тогда ваши бизнес-классы / приложения станут полностью независимыми от инфраструктуры хранилища данных. Вы получаете гибкость в выборе любого хранилища данных и просто подключаете их к базе кода с помощью реализации этого интерфейса.

Вы можете иметь OracleRepository : IRepository и / или MongoRepository : IRepository и введите правильный через IoC как и когда требуется.

Я полностью упускаю суть здесь? Помогите!

Я не думаю, что вы полностью упускаете это, вы на правильном пути, но в этом случае зашли слишком далеко. Все ваши функции CRUD полностью связаны друг с другом, поэтому они принадлежат единому интерфейсу, который несет единую ответственность. Если бы ваш интерфейс подвергал функции CRUD и некоторую другую ответственность, то, по моему мнению, было бы хорошим кандидатом для рефакторинга в отдельные интерфейсы.

Если бы в качестве потребителя вашей функциональности мне пришлось создавать экземпляры разных классов для вставок, удалений и т. Д., Я бы пришел к вам.

Исходя из этого примера, сила разделения каждого типа операции была бы, если бы вы хотели определить возможности объекта на основе комбинаций интерфейсов.

Таким образом, у вас может быть что-то, что только получает, и что-то еще, что получает, сохраняет и удаляет, и что-то еще, что только сохраняет. Затем вы можете передать их в объекты, чьи методы или конструкторы вызывают только ISaves или что-то еще. Таким образом, они не беспокоятся о том, как что-то экономит, а просто сохраняет, что вызывается с помощью метода Save(), предоставляемого интерфейсом.

В качестве альтернативы, у вас может быть сценарий, в котором База данных реализует все интерфейсы, но затем он передается объектам, которые заботятся только о запуске записи, чтения или обновления и т. Д.- и т. Д., Когда он передается этому. Объект передается как соответствующий тип интерфейса, а возможность выполнения других действий не предоставляется потребителю.

Имея это в виду, вполне вероятно, что вашему приложению не нужна такая функциональность. Возможно, вы не пользуетесь данными из разрозненных источников и вам необходимо абстрагироваться от одного общего метода вызова CRUD-операций через них, который решит первый, или от необходимости отделить концепцию базы данных от источника данных, как в противоположность объекту, который поддерживает операции CRUD, и это решит второй. Поэтому, пожалуйста, убедитесь, что это используется для удовлетворения потребностей, а не в попытке следовать передовой практике - потому что это всего лишь один из способов применения определенной практики, но то, является ли она "лучшей", можно определить только в контексте проблемы, которую он решает.

Не совсем ответ, но я хотел бы выложить здесь больше, чем позволял бы комментарий. Такое ощущение, что вы используете шаблон репозитория, поэтому вы можете обернуть все это с помощью 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
}

Теперь вы можете иметь места для расширения функциональности. Приведенный выше пример показывает, как вы можете использовать разные IDataReaders и распределять их по времени, не добавляя журналирование и синхронизацию в каждый IDataReader. Это также показывает, как у вас может быть составной IDataWriter, который может фактически хранить данные в нескольких хранилищах.

Итак, да, абстракция идет с некоторой сантехникой, и может показаться, что она не нужна, но в зависимости от продолжительности жизни вашего проекта, это может сэкономить вам огромную техническую задолженность в будущем.

Вопрос в том, как мне предоставить клиенту интерфейсы 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 контейнер

Когда я проектирую репозитории, я всегда думаю с точки зрения чтения и письма.

Это означает, что я в настоящее время использую эти интерфейсы:

/// <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> - но до сих пор не нашли идеальный путь.

Для Entity Framework я реализую все эти интерфейсы:

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.

Другие вопросы по тегам