Объектно-ориентированный дизайн репозитория — несколько спецификаций

У меня довольно стандартный интерфейс репозитория:

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# поддерживал множественное наследование (с отдельной конкретной реализацией для поиска, добавления, удаления, обновления).




Ответы (1)


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

TDataEntity – это постоянная структура данных, она не имеет значения домена и неизвестна за пределами уровня сохранения. Таким образом, у него могут быть поля, которые он никогда не будет использовать, репозиторий - единственный, кто знает об этом, это detail постоянства. Здесь можно позволить себе «небрежность», на этом уровне все не так важно.

Даже «хакерский» состав — хорошее решение, потому что он находится в одном месте и является личной деталью.

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

P.S: Как правило, общие интерфейсы репозитория хороши, общие абстрактные репозитории не так уж и много (вам нужно быть осторожным), если вы не сериализуете вещи или не используете doc db.

person MikeSW    schedule 15.02.2015