3

域中的所有实体都需要具有身份。通过继承自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);

当使用 a 从持久性中读取城市时CityRepository,将使用第二个构造函数来填充标识属性:

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类型将帮助我解决这个问题,但不幸的是我没有那种奢侈。

4

4 回答 4

4

除了 Ilya Palkin 的回答之外,我还想发布另一个更简单但有点棘手的解决方案:

  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);
    }
}
于 2014-02-07T09:36:10.263 回答
0

我看到以下选项:

  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;
        }
    }
    

    用途:

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

    完整代码可在 GitHub 上作为gist 获得。

  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“实体”类型的方法,因为调用它并不那么明显。

我不认为公共构造函数是邪恶的。当在很多地方调用构造函数并向其中添加新参数会导致无休止的编译错误修复时,这是一种邪恶。我建议您控制调用域实体的构造函数的位置。

于 2014-01-30T16:09:06.187 回答
0

我的感觉是 null id 满足身份 - 即,这是一个的或潜在的实体。我将使用一个构造函数,如下所示:

public City(string name, decimal latitude, decimal longitude, int? id = null) 
    : base(id)
{
    Name = name;
    Coordinate = coordinate;
    SetLocation(latitude, longitude);
}
于 2014-02-02T20:48:21.050 回答
0

最简单的解决方案是使用 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);
    }
}
于 2014-02-09T07:41:02.560 回答