私は DDD の世界にかなり慣れていませんが、それについての本 (Evans DDD など) を何冊か読んだ後、インターネットで私の質問に対する答えを見つけることができませんでした: DDD で子エンティティを作成する適切な方法は何ですか? ご覧のとおり、インターネット上の多くの情報は、単純なレベルで機能しています。しかし、細部の悪魔であり、単純化のために、何十もの DDD サンプルでは常に省略されています。
私は、スタックオーバーフローの類似の質問に関する私自身の回答から来ています。この問題に関する私自身のビジョンに完全に満足しているわけではないので、この問題について詳しく説明する必要があると考えました。
たとえば、車の名前を表す単純なモデルを作成する必要があります: 会社、モデル、およびモディフィケーション (たとえば、Nissan Teana 2012 - これは "Nissan" company、"Teana" モデル、"2012" モディフィケーションになります)。
作成したいモデルのスケッチは次のようになります。
CarsCompany
{
Name
(child entities) Models
}
CarsModel
{
(parent entity) Company
Name
(child entities) Modifications
}
CarsModification
{
(parent entity) Model
Name
}
そのため、コードを作成する必要があります。言語として C# を使用し、ORM として NHibernate を使用します。これは重要なことであり、通常、インターネット上の膨大な 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 はすべてをデータベースに正しく書き込み、次にその会社をロードすると、そのモデル コレクションはモデルを正しく保持します。改造も同様です。したがって、このアプローチでは、親エンティティがデータベースからリロードされるまで一貫性のない状態のままになります。立ち入り禁止。
2番目のアプローチ。
今回は、言語固有のオプションを使用して、他のクラスの保護されたプロパティを設定する問題を解決します。つまり、セッターとコンストラクターの両方で「保護された内部」修飾子を使用します。
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 ();
}
今回は、エンティティを作成するたびに、親エンティティと子エンティティの両方が一貫した状態のままになります。しかし、子エンティティの状態の検証は、親エンティティ (AddModel
およびAddModification
メソッド) に漏れました。私は DDD の専門家ではないので、大丈夫かどうかわかりません。子エンティティのプロパティをプロパティを介して単純に設定できず、渡されたパラメータに基づいて状態を設定するには、パラメータ値をプロパティに割り当てるというより複雑な作業が必要になるため、将来的にさらに多くの問題が発生する可能性があります。可能な限り、そのエンティティ内にエンティティに関するロジックを集中させる必要があるという印象を受けました。私にとって、このアプローチは親オブジェクトをある種のエンティティとファクトリーのハイブリッドに変えます。
3番目のアプローチ。
わかりました、親子関係を維持する責任を逆転させます。
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 で子エンティティを追加する最も正しい方法は何ですか?