これは、ドメイン モデルが専門家が話す言語をドメイン モデルで使用する必要がある場合でも、よく練られた質問です。ドメインの専門家は、ProductConfigurations、ProductOptionsGroups、および Options について話さないと思います。したがって、その分野の専門家 (通常はアプリケーションのターゲット ユーザー) と話をして、そのようなタスクを「紙の上」で行う際に彼が使用する用語を理解する必要があります。
ただし、回答の残りの部分では、ここで使用されている用語が正しいと仮定します。
さらに、私の答えはドメインの説明をモデルにしていますが、別の説明は非常に異なるモデルにつながる可能性があることに注意してください.
境界
付けられたコンテキスト モデル化する 3 つの境界付けられたコンテキストがあります。
- コントラクトのように機能する共通の概念を含む共有カーネル。他の BC は両方ともこれに依存します。
- OptionsGroups とその依存関係の作成と管理に関連するオプションの管理 (
OptionsManagement
この BC にちなんで名付けられた名前空間を使用します)
- 製品の構成の作成と管理に関連する製品の管理 (
ProductsManagement
この BC にちなんで名付けられた名前空間を使用します)
共有カーネルこのステップは簡単です。共有識別子
として機能するいくつかの識別子が必要です。
namespace SharedKernel
{
public struct OptionGroupIdentity : IEquatable<OptionGroupIdentity>
{
private readonly string _name;
public OptionGroupIdentity(string name)
{
// validation here
_name = name;
}
public bool Equals(OptionGroupIdentity other)
{
return _name == other._name;
}
public override bool Equals(object obj)
{
return obj is OptionGroupIdentity
&& Equals((OptionGroupIdentity)obj);
}
public override int GetHashCode()
{
return _name.GetHashCode();
}
public override string ToString()
{
return _name;
}
}
public struct OptionIdentity : IEquatable<OptionIdentity>
{
private readonly OptionGroupIdentity _group;
private readonly int _id;
public OptionIdentity(int id, OptionGroupIdentity group)
{
// validation here
_group = group;
_id = id;
}
public bool BelongTo(OptionGroupIdentity group)
{
return _group.Equals(group);
}
public bool Equals(OptionIdentity other)
{
return _group.Equals(other._group)
&& _id == other._id;
}
public override bool Equals(object obj)
{
return obj is OptionIdentity
&& Equals((OptionIdentity)obj);
}
public override int GetHashCode()
{
return _id.GetHashCode();
}
public override string ToString()
{
return _group.ToString() + ":" + _id.ToString();
}
}
}
オプションの管理には、 という名前の可変エンティティが 1 つだけあります。
これは、このようなもの (持続性、引数チェックなどを含む C# のコード)、例外(および など)、およびグループの状態が変化したときに発生するイベントです。OptionsManagement
OptionGroup
DuplicatedOptionException
MissingOptionException
の部分的な定義は次のOptionGroup
ようになります
public sealed partial class OptionGroup : IEnumerable<OptionIdentity>
{
private readonly Dictionary<OptionIdentity, HashSet<OptionIdentity>> _options;
private readonly Dictionary<OptionIdentity, string> _descriptions;
private readonly OptionGroupIdentity _name;
public OptionGroupIdentity Name { get { return _name; } }
public OptionGroup(string name)
{
// validation here
_name = new OptionGroupIdentity(name);
_options = new Dictionary<OptionIdentity, HashSet<OptionIdentity>>();
_descriptions = new Dictionary<OptionIdentity, string>();
}
public void NewOption(int option, string name)
{
// validation here
OptionIdentity id = new OptionIdentity(option, this._name);
HashSet<OptionIdentity> requirements = new HashSet<OptionIdentity>();
if (!_options.TryGetValue(id, out requirements))
{
requirements = new HashSet<OptionIdentity>();
_options[id] = requirements;
_descriptions[id] = name;
}
else
{
throw new DuplicatedOptionException("Already present.");
}
}
public void Rename(int option, string name)
{
OptionIdentity id = new OptionIdentity(option, this._name);
if (_descriptions.ContainsKey(id))
{
_descriptions[id] = name;
}
else
{
throw new MissingOptionException("OptionNotFound.");
}
}
public void SetRequirementOf(int option, OptionIdentity requirement)
{
// validation here
OptionIdentity id = new OptionIdentity(option, this._name);
_options[id].Add(requirement);
}
public IEnumerable<OptionIdentity> GetRequirementOf(int option)
{
// validation here
OptionIdentity id = new OptionIdentity(option, this._name);
return _options[id];
}
public IEnumerator<OptionIdentity> GetEnumerator()
{
return _options.Keys.GetEnumerator();
}
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}
製品の管理名前空間には、
-以前に選択された一連のオプションを指定して、自身の依存関係をチェックできる値オブジェクト (したがって不変) -指定されたオプションを有効にする必要があるかを決定できるによって識別されるエンティティオプションはすでに有効になっています。- いくつかの例外、永続性など...ProductsManagement
Option
ProductConfiguration
ProductIdentity
次の (非常に簡略化された) コード サンプルで注目できることは、Option
各 の のリストを取得しOptionGroupIdentity
、 を初期化するProductConfiguration
ことは、ドメイン自体の外にあるということです。実際、単純な SQL クエリまたはカスタム アプリケーション コードで両方を処理できます。
namespace ProductsManagement
{
public sealed class Option
{
private readonly OptionIdentity _id;
private readonly OptionIdentity[] _dependencies;
public Option(OptionIdentity id, OptionIdentity[] dependencies)
{
// validation here
_id = id;
_dependencies = dependencies;
}
public OptionIdentity Identity
{
get
{
return _id;
}
}
public bool IsEnabledBy(IEnumerable<OptionIdentity> selectedOptions)
{
// validation here
foreach (OptionIdentity dependency in _dependencies)
{
bool dependencyMissing = true;
foreach (OptionIdentity option in selectedOptions)
{
if (dependency.Equals(option))
{
dependencyMissing = false;
break;
}
}
if (dependencyMissing)
{
return false;
}
}
return true;
}
}
public sealed class ProductConfiguration
{
private readonly ProductIdentity _name;
private readonly OptionGroupIdentity[] _optionsToSelect;
private readonly HashSet<OptionIdentity> _selectedOptions;
public ProductConfiguration(ProductIdentity name, OptionGroupIdentity[] optionsToSelect)
{
// validation here
_name = name;
_optionsToSelect = optionsToSelect;
}
public ProductIdentity Name
{
get
{
return _name;
}
}
public IEnumerable<OptionGroupIdentity> OptionGroupsToSelect
{
get
{
return _optionsToSelect;
}
}
public bool CanBeEnabled(Option option)
{
return option.IsEnabledBy(_selectedOptions);
}
public void Select(Option option)
{
if (null == option)
throw new ArgumentNullException("option");
bool belongToOptionsToSelect = false;
foreach (OptionGroupIdentity group in _optionsToSelect)
{
if (option.Identity.BelongTo(group))
{
belongToOptionsToSelect = true;
break;
}
}
if (!belongToOptionsToSelect)
throw new UnexpectedOptionException(option);
if (!option.IsEnabledBy(_selectedOptions))
throw new OptionDependenciesMissingException(option, _selectedOptions);
_selectedOptions.Add(option.Identity);
}
public void Unselect(Option option)
{
if (null == option)
throw new ArgumentNullException("option");
bool belongToOptionsToSelect = false;
foreach (OptionGroupIdentity group in _optionsToSelect)
{
if (option.Identity.BelongTo(group))
{
belongToOptionsToSelect = true;
break;
}
}
if (!belongToOptionsToSelect)
throw new UnexpectedOptionException(option);
if (!_selectedOptions.Remove(option.Identity))
{
throw new CannotUnselectAnOptionThatWasNotPreviouslySelectedException(option, _selectedOptions);
}
}
}
public struct ProductIdentity : IEquatable<ProductIdentity>
{
private readonly string _name;
public ProductIdentity(string name)
{
// validation here
_name = name;
}
public bool Equals(ProductIdentity other)
{
return _name == other._name;
}
public override bool Equals(object obj)
{
return obj is ProductIdentity
&& Equals((ProductIdentity)obj);
}
public override int GetHashCode()
{
return _name.GetHashCode();
}
public override string ToString()
{
return _name;
}
}
// Exceptions, Events and so on...
}
ドメイン モデルには、このようなビジネス ロジックのみを含める必要があります。
実際、ドメイン モデルが必要になるのは、ビジネス ロジックが十分に複雑であり、残りの適用上の問題 (永続性など) から分離する価値がある場合だけです。アプリケーション全体が何であるかを理解するためにドメインの専門家にお金を払う必要がある場合、ドメイン モデルが必要であることはわかっています。
このような分離を得るためにイベントを使用しますが、他の手法を使用することもできます。
したがって、あなたの質問に答えるには:
依存関係マッピング データを格納する場所
ストレージは DDD ではそれほど重要ではありませんが、最小限の知識の原則に従って、オプションの管理 BC の永続化専用のスキーマにのみ格納します。ドメインおよびアプリケーションのサービスは、必要なときにそのようなテーブルをクエリするだけで済みます。
さらに
マッピングを OptionGroup 集約内に保存しますか? ただし、そうすると、別のユーザーがマッピング データを編集している間に、誰かが OptionGroup の名前と説明を更新した場合、コミット時に同時実行例外が発生します。
実際に会うまで、そのような問題を恐れないでください。それらは、ユーザーに通知する明示的な例外で簡単に解決できます。実際、依存関係の名前が変更されたときに、依存関係を追加するユーザーがコミットの成功を安全と見なすかどうかはわかりません。
これを決定するには、顧客およびドメインの専門家と話し合う必要があります。
ところで、解決策は常に物事を明確にすることです!
編集して新しい質問に答える
にOptionGroup
は、_descriptions
辞書があります。これは、オプションの説明を含めるために使用されます。
オプションの説明プロパティが Option オブジェクトの一部ではないのはなぜですか?
OptionGroup
(またはFeature
) 境界付けられたコンテキストでは、Option
オブジェクトはありません。これは奇妙に見え、最初は間違っているように見えるかもしれませんが、そのコンテキストの Option オブジェクトは、そのコンテキストに付加価値を提供しません。説明を保持するだけでは、クラスを定義するのに十分ではありません。
ただし、私の考えでは、 OptionIdentity には整数ではなく説明が含まれている必要があります。なんで?整数はドメインの専門家には何も言わないからです。「OS:102」は誰にとっても意味がありませんが、「OS:Debian GNU/Linux」はログ、例外、ブレインストーミングで明示されます。
これは、あなたの例の用語をよりビジネス指向のものに置き換えるのと同じ理由です (optionGroup の代わりに機能、オプションの代わりにソリューション、依存関係の代わりに要件)。ドメインの専門家は、それらを正確に表現するために、しばしば不可解な従来型の新しい言語を設計することを余儀なくされました。アプリケーションを構築するには、それを十分に理解する必要があります。
anOption
は値オブジェクトであると述べました。
この場合_id
、タイプのメンバーが呼び出されOptionIdentity
ます。値オブジェクトは識別 ID を持つことができますか?
これは良い質問です。
アイデンティティとは、変化を気にするときに何かを伝えるために使用するものです。オプションの進化を気にしないコンテキストでは、そこでモデル化したいの
は進化だけです。実際、そのコンテキストでは、(またはおそらくより適切な表現で)は、immutable にしたい値です。ProductsManagement
ProductConfiguration
Option
Solution
そのため、Option は値オブジェクトであると言ったのです。そのコンテキストでの「OS:Debian GNU/Linux」の進化については気にしません。その要件が手元の ProductConfiguration によって確実に満たされるようにしたいだけです。
のコードでOption
は、 のコンストラクタid
と のリストを使用しdependencies
ます。
Option
はの一部としてのみ存在することを理解していますOptionGroup
(型にはtypeOptionIdentity
のメンバーが必要であるため)。別の集約インスタンス内にある別のインスタンスへの参照を保持することは許可されていますか? これは、集約ルートへの参照のみを保持し、内部のものを参照しないという DDD 規則に違反していますか?_group
OptionGroupIdentity
Option
Option
OptionGroup
いいえ、それが私が共有識別子モデリング パターンを設計した理由です。
通常、集約ルートとその子エンティティを個別ではなくオブジェクト全体として永続化します。これは、オブジェクト/リスト/辞書を集約ルート内のメンバーとして保持することで行います。Option
コードの場合、 (タイプのOptionIdentity[]
) 依存関係のセットを取ります。
Options
リポジトリからどのように復元されますか? それが別のエンティティに含まれるエンティティである場合、集約ルートの一部として来て、のコンストラクタに渡されるべきではありませんOptionGroup
か?
いいえオプションはエンティティではありません! お値打ちです!
適切なクリーンアップ ポリシーがある場合は、それらをキャッシュできます。ただし、リポジトリによって提供されることはありません。アプリケーションは、必要に応じて次のようなアプリケーション サービスを呼び出してそれらを取得します。
// documentation here
public interface IOptionProvider
{
// documentation here with expected exception
IEnumerable<KeyValuePair<OptionGroupIdentity, string>> ListAllOptionGroupWithDescription();
// documentation here with expected exception
IEnumerable<Option> ListOptionsOf(OptionGroupIdentity group);
// documentation here with expected exception
Option FindOption(OptionIdentity optionEntity)
}