2015-09-18 11 views
3

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

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

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

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

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

+0

Я думаю, что DataReaderSQLite должен реализовать IDataRead not IDataDelete. –

+0

@BigDaddy Мой плохой. Хорошее место. –

+2

ИМХО, вы привносите принцип единственной ответственности в абсурдную крайность. Ответственность класса базы данных должна заключаться в выполнении операции CRUD, не нужно больше ее разбивать. –

ответ

1

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

Идея состоит в том, что класс должен иметь возможность читать сущность из базы данных и обновлять ее новыми значениями. Но класс не должен быть в состоянии получить погоду в Риме и обновить стоимость акций на 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); 
} 

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

Кроме того, если ваши классы Бизнеса/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 по мере необходимости.

+1

Вы понимаете, что 'IRepository ' является нарушением ISP, если каждый потребитель этого интерфейса не использует * всех * своих членов (что очень маловероятно)? И понимаете ли вы, что «EmployeeService» нарушает SRP, реализуя «другую операцию CRUD на сотрудника»? Мне не хватает этой информации из вашего ответа. – Steven

+0

@Steven «что маловероятно», в каких обстоятельствах? Я предполагаю, что каждый репозиторий будет иметь этот минимальный набор операций, как это определено IRepository , чтобы быть полностью функциональным. Теперь это то, что мне нужно в моей работе. Кто-то может подумать, что Query() ему не нужен. В некоторых проектах Delete() может не потребоваться вообще! Так что это один «пример» репозитория, а не «универсальный стандарт» для репозитория. Я не говорю о включении всех этих методов в интерфейс, даже если они им не нужны. –

+0

В службе EmployeeService, нарушающей SRP, SRP является важным шаблоном, который следует соблюдать, чтобы сделать модульные, читаемые, поддерживаемые классы. Надеюсь, вы согласитесь, что создание класса с помощью только одного метода GetEmployee() будет переусердствовать в SRP?Опять же, ожидание здесь - этот класс будет работать только на чтение/запись объектов Employee без каких-либо других операций. Я согласен с тем, что 'EmployeeService' может и не быть правильным именем. –

4

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

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

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

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

3

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

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

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

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 
} 

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

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

+0

Мне это нравится. CompositeWriter - это в основном обработчик в шаблоне обработчика команд. Это позволяет придерживаться Принципа Open/Closed и избегать технического долга, как вы упомянули. – Don

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 легко создать ассиметрический доступ к данным. Это знакомая концепция в архитектуре CQRS. Представьте, что вы хотите отделить свои чтения от записи. И для этого вам не нужно изменять существующий код (благодаря этому вы также придерживаетесь OCP). Что вам нужно сделать, это обеспечить новую реализацию IDataRead интерфейса и зарегистрировать эту реализацию в вашем Dependency Injection контейнере

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> - но до сих пор не нашли способ .

Для 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.

+0

В вашем 'IEntityWriter' интерфейсе вы можете вместо отдельных методов' Create' и 'Update' сделать один метод' Save'. –

+0

Хм, ты прав. Это может быть даже лучше. – janhartmann

 Смежные вопросы

  • Нет связанных вопросов^_^