Несоответствие принципа разделения интерфейса и принципа единой ответственности
Я пытаюсь следовать принципам сегрегации интерфейса и единой ответственности, однако я не понимаю, как все это объединить.
Здесь у меня есть пример нескольких интерфейсов, которые я разделил на меньшие, более направленные интерфейсы:
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.