使用 DDD 创建子实体的正确方法

Posted

技术标签:

【中文标题】使用 DDD 创建子实体的正确方法【英文标题】:Proper way of creating child entities with DDD 【发布时间】:2012-06-16 19:59:51 【问题描述】:

我对 DDD 世界还很陌生,在阅读了几本关于它的书(其中包括 Evans DDD)后,我无法在互联网上找到我的问题的答案:用 DDD 创建子实体的正确方法是什么?你看,互联网上的很多信息都在一些简单的层面上运作。但是细节上的魔鬼,为了简单起见,在几十个 DDD 示例中总是省略它们。

我来自my own answer,关于*** 上的类似问题。我对自己对这个问题的看法并不完全满意,所以我认为我需要详细说明这个问题。

例如,我需要创建代表汽车命名的简单模型:公司、型号和改装(例如,Nissan Teana 2012 - 这将是“Nissan”公司、“Teana”型号和“2012”改装)。

我要创建的模型草图如下所示:

CarsCompany

    Name
    (child entities) Models


CarsModel

    (parent entity) Company
    Name
    (child entities) Modifications



CarsModification

    (parent entity) Model
    Name

所以,现在我需要创建代码。我将使用 C# 作为语言,使用 NHibernate 作为 ORM。这很重要,并且通常不会在 Internet 上的大量 DDD 示例中显示。

第一种方法。

我将从简单的方法开始,通过工厂方法创建典型的对象。

public class CarsCompany

    public virtual string Name  get; protected set; 
    public virtual IEnumerable<CarsModel> Models  get  return new ImmutableSet<CarsModel> (this._models);  


    private readonly ISet<CarsModel> _models = new HashedSet<CarsModel> ();


    protected CarsCompany ()
    
    


    public static CarsCompany Create (string name)
    
        if (string.IsNullOrEmpty (name))
            throw new ArgumentException ("Invalid name specified.");

        return new CarsCompany
        
            Name = name
        ;
    


    public void AddModel (CarsModel model)
    
        if (model == null)
            throw new ArgumentException ("Model is not specified.");

        this._models.Add (model);
    



public class CarsModel

    public virtual CarsCompany Company  get; protected set; 
    public virtual string Name  get; protected set; 
    public virtual IEnumerable<CarsModification> Modifications  get  return new ImmutableSet<CarsModification> (this._modifications);  


    private readonly ISet<CarsModification> _modifications = new HashedSet<CarsModification> ();


    protected CarsModel ()
    
    


    public static CarsModel Create (CarsCompany company, string name)
    
        if (company == null)
            throw new ArgumentException ("Company is not specified.");

        if (string.IsNullOrEmpty (name))
            throw new ArgumentException ("Invalid name specified.");

        return new CarsModel
        
            Company = company,
            Name = name
        ;
    


    public void AddModification (CarsModification modification)
    
        if (modification == null)
            throw new ArgumentException ("Modification is not specified.");

        this._modifications.Add (modification);
    



public class CarsModification

    public virtual CarsModel Model  get; protected set; 
    public virtual string Name  get; protected set; 


    protected CarsModification ()
    
    


    public static CarsModification Create (CarsModel model, string name)
    
        if (model == null)
            throw new ArgumentException ("Model is not specified.");

        if (string.IsNullOrEmpty (name))
            throw new ArgumentException ("Invalid name specified.");

        return new CarsModification
        
            Model = model,
            Name = name
        ;
    

这种方法的坏处是模型的创建不会将其添加到父模型集合中:

using (var tx = session.BeginTransaction ())

    var company = CarsCompany.Create ("Nissan");

    var model = CarsModel.Create (company, "Tiana");
    company.AddModel (model);
    // (model.Company == company) is true
    // but (company.Models.Contains (model)) is false

    var modification = CarsModification.Create (model, "2012");
    model.AddModification (modification);
    // (modification.Model == model) is true
    // but (model.Modifications.Contains (modification)) is false

    session.Persist (company);
    tx.Commit ();

事务提交并刷新会话后,ORM 将正确地将所有内容写入数据库,下次我们加载该公司时,它的模型集合将正确保存我们的模型。修改也是如此。所以这种方法让我们的父实体处于不一致的状态,直到它从数据库中重新加载。不行。

第二种方法。

这一次我们将使用特定于语言的选项来解决设置其他类的受保护属性的问题 - 即我们将在设置器和构造器上使用“受保护的内部”修饰符。

public class CarsCompany

    public virtual string Name  get; protected set; 
    public virtual IEnumerable<CarsModel> Models  get  return new ImmutableSet<CarsModel> (this._models);  


    private readonly ISet<CarsModel> _models = new HashedSet<CarsModel> ();


    protected CarsCompany ()
    
    


    public static CarsCompany Create (string name)
    
        if (string.IsNullOrEmpty (name))
            throw new ArgumentException ("Invalid name specified.");

        return new CarsCompany
        
            Name = name
        ;
    


    public CarsModel AddModel (string name)
    
        if (string.IsNullOrEmpty (name))
            throw new ArgumentException ("Invalid name specified.");

        var model = new CarsModel
        
            Company = this,
            Name = name
        ;

        this._models.Add (model);

        return model;
    



public class CarsModel

    public virtual CarsCompany Company  get; protected internal set; 
    public virtual string Name  get; protected internal set; 
    public virtual IEnumerable<CarsModification> Modifications  get  return new ImmutableSet<CarsModification> (this._modifications);  


    private readonly ISet<CarsModification> _modifications = new HashedSet<CarsModification> ();


    protected internal CarsModel ()
    
    


    public CarsModification AddModification (string name)
    
        if (string.IsNullOrEmpty (name))
            throw new ArgumentException ("Invalid name specified.");

        var modification = new CarsModification
        
            Model = this,
            Name = name
        ;

        this._modifications.Add (modification);

        return modification;
    



public class CarsModification

    public virtual CarsModel Model  get; protected internal set; 
    public virtual string Name  get; protected internal set; 


    protected internal CarsModification ()
    
    


...

using (var tx = session.BeginTransaction ())

    var company = CarsCompany.Create ("Nissan");
    var model = company.AddModel ("Tiana");
    var modification = model.AddModification ("2011");

    session.Persist (company);
    tx.Commit ();

这一次,每次实体创建都会使父实体和子实体保持一致状态。但是子实体状态的验证泄漏到父实体中(AddModelAddModification 方法)。由于我不是 DDD 方面的专家,我不确定它是否可以。当子实体的属性不能简单地通过属性设置并且基于传递的参数设置一些状态需要更复杂的工作来为属性分配参数值时,它可能会在未来产生更多问题。我的印象是,我们应该尽可能地将关于实体的逻辑集中在该实体内部。对我来说,这种方法将父对象变成了某种实体和工厂的混合体。

第三种方法。

好的,我们将颠倒维护父子关系的职责。

public class CarsCompany

    public virtual string Name  get; protected set; 
    public virtual IEnumerable<CarsModel> Models  get  return new ImmutableSet<CarsModel> (this._models);  


    private readonly ISet<CarsModel> _models = new HashedSet<CarsModel> ();


    protected CarsCompany ()
    
    


    public static CarsCompany Create (string name)
    
        if (string.IsNullOrEmpty (name))
            throw new ArgumentException ("Invalid name specified.");

        return new CarsCompany
        
            Name = name
        ;
    


    protected internal void AddModel (CarsModel model)
    
        this._models.Add (model);
    



public class CarsModel

    public virtual CarsCompany Company  get; protected set; 
    public virtual string Name  get; protected set; 
    public virtual IEnumerable<CarsModification> Modifications  get  return new ImmutableSet<CarsModification> (this._modifications);  


    private readonly ISet<CarsModification> _modifications = new HashedSet<CarsModification> ();


    protected CarsModel ()
    
    


    public static CarsModel Create (CarsCompany company, string name)
    
        if (company == null)
            throw new ArgumentException ("Company is not specified.");

        if (string.IsNullOrEmpty (name))
            throw new ArgumentException ("Invalid name specified.");

        var model = new CarsModel
        
            Company = company,
            Name = name
        ;

        model.Company.AddModel (model);

        return model;
    


    protected internal void AddModification (CarsModification modification)
    
        this._modifications.Add (modification);
    



public class CarsModification

    public virtual CarsModel Model  get; protected set; 
    public virtual string Name  get; protected set; 


    protected CarsModification ()
    
    


    public static CarsModification Create (CarsModel model, string name)
    
        if (model == null)
            throw new ArgumentException ("Model is not specified.");

        if (string.IsNullOrEmpty (name))
            throw new ArgumentException ("Invalid name specified.");

        var modification = new CarsModification
        
            Model = model,
            Name = name
        ;

        modification.Model.AddModification (modification);

        return modification;
    


...

using (var tx = session.BeginTransaction ())

    var company = CarsCompany.Create ("Nissan");
    var model = CarsModel.Create (company, "Tiana");
    var modification = CarsModification.Create (model, "2011");

    session.Persist (company);
    tx.Commit ();

这种方法在相应实体中获取了所有验证/创建逻辑,我不知道它是好是坏,但是通过使用工厂方法简单地创建对象,我们隐式地将其添加到父对象子集合中。在事务提交和会话刷新之后,即使我从未在我的代码中编写过一些“添加”命令,也会有 3 次插入到数据库中。我不知道可能只是我和我在 DDD 世界之外的丰富经验,但现在感觉有点不自然。

那么,用 DDD 添加子实体的最正确方法是什么?

【问题讨论】:

我可以删除所有代码示例,让整个问题更难理解——但我认为这对社区没有帮助。我不知道 SO 不适用于复杂的问题。 整个问题(有或没有代码)在这里都不合适。正如我所说,它要求讨论。阅读我发布的链接。我可以提供另一个:Not a discussion board or forum。正如我之前所说,这个网站是针对无需讨论即可回答的清晰、简洁的问题。由于您的整个问题都是关于讨论“最正确的方式”并要求“听取意见 [sp]”(这在这里也不合适),所以这里不合适。 我在问非常具体的问题,并等待一个非常具体的答案。通过我的例子,我只是展示了我在这个问题上所做的研究工作。我不需要任何讨论——我需要明确回答如何正确地做这件事。仅仅因为你不知道这个问题的答案并不意味着它具有讨论性质。 DDD 世界使用许多模式,我要求指出一个我应该应用来解决这个特定问题的模式。 【参考方案1】:

那么,用 DDD 添加子实体的最正确方法是什么?

第三种方法称为Tight Coupling。 CompanyCarModification 几乎了解彼此的一切。

第二种方法在 DDD 中被广泛提出。域对象负责创建嵌套域对象并将其注册到内部。

第一种方法是经典的 OOP 风格。对象的创建与将对象添加到某个集合中是分开的。这样,代码使用者可以用任何派生类(例如 TrailerCar)的对象替换具体类(例如 Car)的对象。

// var model = CarsModel.Create (company, "Tiana");

var model = TrailerCarsModel.Create (
    company, "Tiana", SimpleTrailer.Create(company));

company.AddModel (model);

尝试在第 2 种/第 3 种方法中采用这种业务逻辑更改。

【讨论】:

【参考方案2】:

我在这里得到了可以接受的答案:https://groups.yahoo.com/neo/groups/domaindrivendesign/conversations/messages/23187

基本上,它是方法 2 和 3 的组合 - 将 AddModel 方法放入 CarsCompany 并使其调用 CarsModel 的受保护的内部构造函数,其名称参数在 CarsModel 的构造函数中验证。

【讨论】:

很有趣,但是在哪里向父子集合添加对象? 在父 .AddChild 方法中,通过受保护的内部构造函数创建子节点(将 self 作为参数传递给子节点以在构造函数中设置其父节点),将其添加到子集合并返回。 谢谢,有道理。您是否将此模式与实体框架结合使用? 实际上我离开了 DDD,因为我意识到典型 Web 应用程序的规模和 CRUD 特性并不能保证使用纯 DDD 的开销并忘记你正在使用真正的数据库(这很慢而且所有当前的 ORM 都不是那么有效)。 错误描述:无法处理这个“GET”请求。这就是 *** 101 为什么你应该包含答案而不是链接到其他网站。【参考方案3】:

有趣。 DDD 与 Repository / ORM 导航属性。我认为答案取决于您是处理一个聚合还是两个聚合。 CarsModel 应该是 CarsCompany 聚合的一部分,还是它自己的聚合?

方法一是让问题消失。 MikeSW 暗示了这一点。如果 CarsCompany 和 CarsModel 不需要属于同一个聚合,那么它们应该只通过身份相互引用,导航属性在域中不可见。

方法二是像对待获取聚合一样对待添加到关系中 - 让应用程序服务从存储库中调用一个方法,这是解决您的 ORM 特定问题的正确位置。这种方法可以填充关系的两端。

【讨论】:

【参考方案4】:

这是一个非常具体和残酷诚实的答案:你所有的方法都是错误的,因为你违反了 DDD 的“第一条规则”,即数据库不存在。

您定义的是 ORM (nhibernate) 的 PERSISTENCE 模型。为了设计域对象,首先您必须识别Bounded Context、它的模型、该模型的实体和值对象以及Aggregate Root(它将在内部处理子规则和业务规则)。

Nhibernate 或 db 模式在这里没有位置,您只需要纯 C# 代码和对领域的清晰了解。

【讨论】:

是的,这一切都很好,听起来就像任何 DDD 书籍一样,但这里的问题不在于设计域。实施中的问题。在开发中,我们有一个非常具体的模式来解决一个非常具体的问题——一个将域伪造到工作代码中的问题。我要求指出实现它的正确方法。我可以用纯虚构的语言来写它,它可以在那个虚构的世界中工作(我对此没有问题)。 难点在于设计域。事实上,建立 BC 和 Aggregates 是最棘手的事情。一旦你知道它们(在伪代码中),用 C# 编写它们只是一种形式。这不是理论,DDD 最难的部分是理解 DOMAIN 并非常清楚地区分哪个是哪个。也没有银弹或任何可应用的模式。 DDD 是关于概念和指南的。你想在 Nhibernate 中实现一些关系。那不是 DDD。 您需要 DOMAIN EXPERT 来帮助您了解问题和细节。我或周围的其他人都不是领域专家。您必须与专家交谈才能清楚地了解要解决的问题。老实说,如果不清楚地了解 BC,您就无法设计或实现实体或聚合。 MikeSW,为什么你不花时间举个例子,而不仅仅是说话。我分享了 FuriCuri 对这件事的怀疑,关于 DDD 一切都很好,但是当你不得不把手放在代码中时,一切都会改变。最重要的是我们如何设计域以便以后设计 DAO 和存储库。这就是在询问 furicuri,如何为亲子关系正确设计 POJO。 投反对票:问题是关于做 DDD 和使用 ORM 时的一个常见问题。说“我不是你的领域专家”并没有多大帮助。

以上是关于使用 DDD 创建子实体的正确方法的主要内容,如果未能解决你的问题,请参考以下文章

如何在couchbase服务器中正确创建子查询的索引?

插入新实体而不创建子实体(如果存在)

使用 web api 发布/创建子实体对象时如何设置父实体 ID

线程关联性:无法为位于不同线程中的父级创建子级

DDD 域实体与持久性实体

支持 DDD 的企业应用框架