Установка идентичности доменного объекта
Все сущности в домене должны иметь идентичность. Наследуя от 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 ответа
В дополнение к ответу Ильи Палкина я хочу опубликовать еще одно решение, которое проще, но немного сложнее:
- Делать
DomainEntity.UniqueId
защищены, так что к нему можно получить доступ от своих детей - Введите фабрику (или статический метод фабрики) и определите ее внутри класса 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);
}
}
Я вижу следующие варианты:
Статический метод на
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);
Отражение можно использовать для получения доступа к приватному полю
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.
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);
}