Установка идентичности доменного объекта

Все сущности в домене должны иметь идентичность. Наследуя от DomainEntity Я могу предоставить личность для классов.

Сущность городского домена (для упрощения чтения):

public class City : DomainEntity, IAggregateRoot
{
    public string Name { get; private set; }

    public Coordinate Coordinate { get; private set; }

    public City(string name, decimal latitude, decimal longitude) 
    {
        Name = name;
        SetLocation(latitude, longitude);
    }

    public City(string name, decimal latitude, decimal longitude, int id) 
        : base(id)
    {
        Name = name;
        Coordinate = coordinate;
        SetLocation(latitude, longitude);
    }

    public void SetLocation(decimal latitude, decimal longitude)
    {
        Coordinate = new Coordinate(latitude, longitude);
    }
}

Абстрактный класс DomainEntity:

public abstract class DomainEntity
{
    private int? uniqueId;

    public int Id
    {
        get
        {
            return uniqueId.Value;
        }
    }

    public DomainEntity()
    { }

    public DomainEntity(int id)
    {
        uniqueId = id;
    }
}

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

var city = new City("Cape Town", 18.42, -33.92);

Когда города читаются из постоянства, используя CityRepository, тогда второй конструктор будет использоваться для заполнения свойства identity:

public class CityRepository : ICityRepository
{
    public City Find(int id)
    {
        var cityTblEntity = context.Set<CityTbl>().Find(id);

        return new City(cityTblEntity.Name, cityTblEntity.Lat, cityTblEntity.Long, cityTblEntity.Id);
    }
}

У меня проблема в том, что я предоставляю конструктор, который может принимать идентичность. Это открывает дыру. Я только хочу, чтобы идентичность была установлена ​​на уровне хранилища, но клиентский код теперь может также начать настройку Id ценности. Что мешает кому-то сделать это:

var city = new City("Cape Town", 18.42, -33.92, 99999);  // What is 99999? It could even be an existing entity!

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

Примечание: я понимаю, что это не идеальная реализация DDD, поскольку сущности должны иметь идентичность с самого начала. Guid type поможет мне решить эту проблему, но, к сожалению, такой роскоши у меня нет.

4 ответа

Решение

В дополнение к ответу Ильи Палкина я хочу опубликовать еще одно решение, которое проще, но немного сложнее:

  1. Делать DomainEntity.UniqueId защищены, так что к нему можно получить доступ от своих детей
  2. Введите фабрику (или статический метод фабрики) и определите ее внутри класса City, чтобы он мог получить доступ к DomainEntity.UniqueId защищенное поле.

Плюсы: без размышлений, код тестируемый.
Минусы: Доменный уровень знает о уровне DAL. Немного хитрое определение фабрики.

Код:

public abstract class DomainEntity
{
    // Set UniqueId to protected, so you can access it from childs
    protected int? UniqueId;
}

public class City : DomainEntity
{
    public string Name { get; private set; }

    public City(string name)
    {
        Name = name;
    }

    // Introduce a factory that creates a domain entity from a table entity
    // make it internal, so you can access only from defined assemblies 
    // also if you don't like static you can introduce a factory class here
    // just put it inside City class definition
    internal static City CreateFrom(CityTbl cityTbl)
    {
        var city = new City(cityTbl.Name); // or use auto mapping
        // set the id field here
        city.UniqueId = cityTbl.Id;
        return city;
    }
}

public class CityTbl
{
    public int Id { get; set; }
    public string Name { get; set; }
}

static void Main()
{
    var city = new City("Minsk");

    // can't access UniqueId and factory from a different assembly
    // city.UniqueId = 1;
    // City.CreateFrom(new CityTbl());
}

// Your repository will look like
// and it won't know about how to create a domain entity which is good in terms of SRP
// You can inject the factory through constructor if you don't like statics
// just put it inside City class
public class CityRepository : ICityRepository
{
    public City Find(int id)
    {
        var cityTblEntity = context.Set<CityTbl>().Find(id);

        return City.CreateFrom(cityTblEntity);
    }
}

Я вижу следующие варианты:

  1. Статический метод на Entity Тип, который имеет доступ к закрытым полям.

    public class Entity
    {
        private int? id;            
        /* ... */           
        public static void SetId(Entity entity, int id)
        {
            entity.id = id;
        }
    }
    

    Использование:

        var target = new Entity();
        Entity.SetId(target, 100500);
    
  2. Отражение можно использовать для получения доступа к приватному полю

    public static class EntityHelper
    {
        public static TEntity WithId<TEntity>(this TEntity entity, int id)
            where TEntity : Entity
        {
            SetId(entity, id);
            return entity;
        }
    
        private static void SetId<TEntity>(TEntity entity, int id)
            where TEntity : Entity
        {
            var idProperty = GetField(entity.GetType(), "id", BindingFlags.NonPublic | BindingFlags.Instance);
            /* ... */   
            idProperty.SetValue(entity, id);
        }
    
        public static FieldInfo GetField(Type type, string fieldName, BindingFlags bindibgAttr)
        {
            return type != null
                ? (type.GetField(fieldName, bindibgAttr) ?? GetField(type.BaseType, fieldName, bindibgAttr))
                : null;
        }
    }
    

    Usege:

        var target = new Entity().WithId(100500);
    

    Полный код доступен на GitHub.

  3. Automapper можно использовать, поскольку он использует отражение и может отображать частные свойства.

    Я проверил, отвечая, как получить объект домена из хранилища

    [TestClass]
    public class AutomapperTest
    {
        [TestMethod]
        public void Test()
        {
            // arrange
            Mapper.CreateMap<AModel, A>();
            var model = new AModel { Value = 100 };
    
            //act
            var entity = Mapper.Map<A>(model);
    
            // assert
            entity.Value.Should().Be(100);
            entity.Value.Should().Be(model.Value);
        }
    }
    
    public class AModel
    {
        public int Value { get; set; }
    }
    
    public class A
    {
        public int Value { get; private set; }
    } 
    

PS: реализация DomainEntity.Id может привести к InvalidOperationException когда uniqueId не установлен.

РЕДАКТИРОВАТЬ:

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

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

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

Фабрики имеют ряд преимуществ перед конструкторами:

  • Фабрики могут рассказать об объектах, которые они создают
  • Фабрики полиморфны в том смысле, что могут возвращать объект или любой подтип создаваемого объекта.
  • В случаях, когда создаваемый объект имеет много необязательных параметров, мы можем иметь объект Builder в качестве Фабрики

Было бы здорово, если бы я мог использовать фабрики для создания новых экземпляров с идентичностью, но разве этим фабрикам все еще не нужно было бы обращаться к этим публичным конструкторам, обремененным идентичностью?

Я думаю что то public необходимо в любом случае, если вы не используете отражение.

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

    public void Apply(AppointmentCreatedFact fact)
    {
        Id = fact.Id;
        DateOfAppointment = fact.DateOfAppointment;
    }

Но я предпочитаю static метод типа "Entity", так как его не так очевидно вызывать.

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

Самое простое решение - сделать все конструкторы с Id внутренним, что требует минимальных изменений.

public class City : DomainEntity, IAggregateRoot
{
    public City(string name, decimal latitude, decimal longitude)
    {
        Name = name;
        SetLocation(latitude, longitude);
    }

    // Just make it internal
    internal City(string name, decimal latitude, decimal longitude, int id)
        : base(id)
    {
        Name = name;
        Coordinate = coordinate;
        SetLocation(latitude, longitude);
    }
}

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

public City(string name, decimal latitude, decimal longitude, int? id = null) 
    : base(id)
{
    Name = name;
    Coordinate = coordinate;
    SetLocation(latitude, longitude);
}
Другие вопросы по тегам