У меня довольно стандартный интерфейс репозитория:
public interface IRepository<TDomainEntity>
where TDomainEntity : DomainEntity, IAggregateRoot
{
TDomainEntity Find(Guid id);
void Add(TDomainEntity entity);
void Update(TDomainEntity entity);
}
Мы можем использовать различные реализации инфраструктуры, чтобы обеспечить функциональность по умолчанию (например, Entity Framework, DocumentDb, Table Storage и т. д.). Вот как выглядит реализация Entity Framework (для простоты без кода EF):
public abstract class EntityFrameworkRepository<TDomainEntity, TDataEntity> : IRepository<TDomainEntity>
where TDomainEntity : DomainEntity, IAggregateRoot
where TDataEntity : class, IDataEntity
{
protected IEntityMapper<TDomainEntity, TDataEntity> EntityMapper { get; private set; }
public TDomainEntity Find(Guid id)
{
// Find, map and return entity using Entity Framework
}
public void Add(TDomainEntity item)
{
var entity = EntityMapper.CreateFrom(item);
// Insert entity using Entity Framework
}
public void Update(TDomainEntity item)
{
var entity = EntityMapper.CreateFrom(item);
// Update entity using Entity Framework
}
}
Существует сопоставление между сущностью домена TDomainEntity
(агрегат) и сущностью данных TDataEntity
Entity Framework (таблица базы данных). Я не буду вдаваться в подробности, почему существуют отдельные объекты домена и данных. Это философия проектирования, управляемого предметной областью (читайте об агрегатах). Здесь важно понимать, что репозиторий будет предоставлять доступ только к объекту домена.
Чтобы создать новый репозиторий, скажем, для «пользователей», я мог бы определить интерфейс следующим образом:
public interface IUserRepository : IRepository<User>
{
// I can add more methods over and above those in IRepository
}
А затем используйте реализацию Entity Framework, чтобы предоставить базовые функции Find
, Add
и Update
для агрегата:
public class UserRepository : EntityFrameworkRepository<Stop, StopEntity>, IUserRepository
{
// I can implement more methods over and above those in IUserRepository
}
Приведенное выше решение отлично сработало. Но теперь мы хотим реализовать функцию удаления. Я предложил следующий интерфейс (это IRepository
):
public interface IDeleteableRepository<TDomainEntity>
: IRepository<TDomainEntity>
{
void Delete(TDomainEntity item);
}
Класс реализации Entity Framework теперь будет выглядеть примерно так:
public abstract class EntityFrameworkRepository<TDomainEntity, TDataEntity> : IDeleteableRepository<TDomainEntity>
where TDomainEntity : DomainEntity, IAggregateRoot
where TDataEntity : class, IDataEntity, IDeleteableDataEntity
{
protected IEntityMapper<TDomainEntity, TDataEntity> EntityMapper { get; private set; }
// Find(), Add() and Update() ...
public void Delete(TDomainEntity item)
{
var entity = EntityMapper.CreateFrom(item);
entity.IsDeleted = true;
entity.DeletedDate = DateTime.UtcNow;
// Update entity using Entity Framework
// ...
}
}
Как определено в приведенном выше классе, универсальный TDataEntity
теперь также должен иметь тип IDeleteableDataEntity
, для чего требуются следующие свойства:
public interface IDeleteableDataEntity
{
bool IsDeleted { get; set; }
DateTime DeletedDate { get; set; }
}
Эти свойства установлены соответствующим образом в реализации Delete()
.
Это означает, что, ЕСЛИ требуется, я могу определить IUserRepository
с возможностями «удаления», о которых позаботится соответствующая реализация:
public interface IUserRepository : IDeleteableRepository<User>
{
}
При условии, что соответствующий объект данных Entity Framework является IDeleteableDataEntity
, это не будет проблемой.
Самое замечательное в этом дизайне то, что я могу еще больше детализировать модель репозитория (IUpdateableRepository
, IFindableRepository
, IDeleteableRepository
, IInsertableRepository
), и агрегированные репозитории теперь могут предоставлять только соответствующие функции в соответствии с нашей спецификацией (возможно, вам должно быть разрешено вставлять в UserRepository
, но НЕ в ClientRepository
). Кроме того, он определяет стандартизированный способ выполнения определенных действий с репозиторием (т. е. обновление столбцов IsDeleted
и DeletedDate
будет универсальным и не зависит от разработчика).
ПРОБЛЕМА
Проблема с приведенным выше дизайном возникает, когда я хочу создать репозиторий для некоторых агрегатов БЕЗ возможности удаления, например:
public interface IClientRepository : IRepository<Client>
{
}
Реализация EntityFrameworkRepository
по-прежнему требует, чтобы TDataEntity
имел тип IDeleteableDataEntity
.
Я могу гарантировать, что модель объекта данных клиента реализует IDeleteableDataEntity
, но это неверно и вводит в заблуждение. Будут дополнительные поля, которые никогда не обновляются.
Единственное решение, которое я могу придумать, это удалить общее условие IDeleteableDataEntity
из TDataEntity
, а затем привести к соответствующему типу в методе Delete()
:
public abstract class EntityFrameworkRepository<TDomainEntity, TDataEntity> : IDeleteableRepository<TDomainEntity>
where TDomainEntity : DomainEntity, IAggregateRoot
where TDataEntity : class, IDataEntity
{
protected IEntityMapper<TDomainEntity, TDataEntity> EntityMapper { get; private set; }
// Find() and Update() ...
public void Delete(TDomainEntity item)
{
var entity = EntityMapper.CreateFrom(item);
var deleteableEntity = entity as IDeleteableEntity;
if(deleteableEntity != null)
{
deleteableEntity.IsDeleted = true;
deleteableEntity.DeletedDate = DateTime.UtcNow;
entity = deleteableEntity;
}
// Update entity using Entity Framework
// ...
}
}
Поскольку ClientRepository
не реализует IDeleteableRepository
, метод Delete()
не будет раскрыт, и это хорошо.
ВОПРОС
Может ли кто-нибудь посоветовать лучшую архитектуру, которая использует систему типизации С# и не требует хакерского приведения?
Интересно, что я мог бы сделать это, если бы C# поддерживал множественное наследование (с отдельной конкретной реализацией для поиска, добавления, удаления, обновления).