6

チェックアウト プロセスを作成していますが、その 1 つのステップには製品の構成が含まれます。使用例は次のとおりです。

製品構成

製品構成は、構成可能なオプション グループのセットです。

オプション グループ

各オプション グループは、1 つの選択されたオプション (または何も選択されていない) で構成され、グループは複数のオプションで構成されます。

ユーザーは、製品グループのオプションを追加および削除できます。

例として、オプション グループはデータベースと呼ばれることがあります。

オプション

オプションは、オプション グループの特定のオプションです。

データベース オプション グループに属するオプションの例として、特定のオプションは MySQL または MS-SQL である可能性があります。

オプション グループの依存関係 オプション グループは、他の 1 つのオプション グループに依存関係を持つことができるため、ターゲット オプション グループの要件が満たされない場合、特定の項目が除外されます。

ターゲットの依存関係は 1 つだけです。複数のターゲット製品オプション グループを指す製品オプション グループのオプションについて心配する必要はありません。

たとえば、データベース製品グループで MS-SQL オプションを選択できるようにするには、オペレーティング システム オプション グループから Windows オプションを選択する必要があります。

同様に、データベース製品グループで MySQL オプションを選択できるようにするには、オペレーティング システム オプション グループから Windows または Linux オプションを選択する必要があります。

構造

ここに画像の説明を入力

上記の図では、MySQL (ID = 201) 製品オプションは、OS 製品オプション グループの Windows (ID = 101) または Linux (ID = 102) 製品オプションに依存しています。これらのオペレーティング システム オプションのいずれかを選択すると、MySQL が表示されます。

MS-SQL (ID = 202) 製品オプションは、OS 製品オプション グループの Windows (ID = 101) 製品オプションに依存しています。Windows オペレーティング システムが選択されている場合のみ、MS-SQL が表示されます。

質問 - 依存関係マッピング データを保存する場所は?

コードが進化する現在の問題は、製品オプションとそのグループ間の関係依存関係マッピングをどこに保存するかです。私が質問している主な問題は次のとおりです。

分別集計、取引管理

マッピングを独自の集計に保存しますか? 保存する場合、参照されている製品と ProductOptionGroups の削除をどのように検出して停止しますか?

たとえば、オペレーティング システム Windows に依存関係がある場合、それを保護する必要があり、他の OptionGroup が依存関係を持っている場合は、OS ProductOptionGroup からの削除を許可しません。

これはアプリケーション サービスによって行われますか? コードでトランザクションを構築するにはどうすればよいでしょうか?

集計内で、トランザクション管理が容易になり、同時実行の問題が発生する可能性が高くなります

マッピングを OptionGroup 集約内に保存しますか? ただし、そうすると、別のユーザーがマッピング データを編集している間に、誰かが OptionGroup の名前と説明を更新した場合、コミット時に同時実行例外が発生します。

誰かが名前を更新してもデータのマッピングが失敗してはならないため、これは実際には意味がありません。これらは 2 つの無関係な概念です。

この状況で他の人は何をしますか?また、上記のシナリオのコードをどのように構成するのが最善でしょうか? それとも、再設計すれば物事が簡単になるという集合体から私を見つめているより深い洞察が欠けているのでしょうか.

外部から ProductOptionGroup 内の ProductOptions にアクセスすることは DDD 設計によって禁止されていると思いますが、現時点では他の方法でモデル化する方法は考えられません。

Giacomo Tesio の提案された回答の編集

提案された回答と、時間を割いて助けてくれてありがとう。きちんとした簡潔なコーディング スタイルがとても気に入っています。あなたの答えは、以下のようにさらにいくつかの質問を提起します。私は間違ったツリーを吠えている可能性がありますが、明確化していただければ幸いです

  1. OptionGroupは、_descriptions辞書があります。これは、オプションの説明を含めるために使用されます。

    オプションの説明プロパティが Option オブジェクトの一部ではないのはなぜですか?

  2. anOptionは値オブジェクトであると述べました。

    この場合_id、タイプのメンバーが呼び出されOptionIdentityます。値オブジェクトは識別 ID を持つことができますか?

  3. のコードでOptionは、 のコンストラクタidと のリストを使用しdependenciesます。

    Optionはの一部としてのみ存在することを理解していますOptionGroup(型にはtypeOptionIdentityのメンバーが必要であるため)。別の集約インスタンス内にある別のインスタンスへの参照を保持することは許可されていますか? これは、集約ルートへの参照のみを保持し、内部のものを参照しないという DDD 規則に違反していますか?_groupOptionGroupIdentityOptionOptionOptionGroup

  4. 通常、集約ルートとその子エンティティを個別ではなくオブジェクト全体として永続化します。これは、オブジェクト/リスト/辞書を集約ルート内のメンバーとして保持することで行います。Optionコードの場合、 (タイプのOptionIdentity[]) 依存関係のセットを取ります。

    Optionsリポジトリからどのように復元されますか? それが別のエンティティに含まれるエンティティである場合、集約ルートの一部として来て、のコンストラクタに渡されるべきではありませんOptionGroupか?

4

1 に答える 1

5

これは、ドメイン モデルが専門家が話す言語をドメイン モデルで使用する必要がある場合でも、よく練られた質問です。ドメインの専門家は、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# のコード)、例外(および など)、およびグループの状態が変化したときに発生するイベントです。OptionsManagementOptionGroupDuplicatedOptionExceptionMissingOptionException

の部分的な定義は次の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();
    }
}

製品の管理名前空間には、
-以前に選択された一連のオプションを指定して、自身の依存関係をチェックできる値オブジェクト (したがって不変) -指定されたオプションを有効にする必要があるかを決定できるによって識別されるエンティティオプションはすでに有効になっています。- いくつかの例外、永続性など...ProductsManagementOptionProductConfigurationProductIdentity

次の (非常に簡略化された) コード サンプルで注目できることは、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 の名前と説明を更新した場合、コミット時に同時実行例外が発生します。

実際に会うまで、そのような問題を恐れないでください。それらは、ユーザーに通知する明示的な例外で簡単に解決できます。実際、依存関係の名前が変更されたときに、依存関係を追加するユーザーがコミットの成功を安全と見なすかどうかはわかりません。

これを決定するには、顧客およびドメインの専門家と話し合う必要があります。

ところで、解決策は常に物事を明確にすることです!

編集して新しい質問に答える

  1. OptionGroupは、_descriptions辞書があります。これは、オプションの説明を含めるために使用されます。

    オプションの説明プロパティが Option オブジェクトの一部ではないのはなぜですか?

OptionGroup(またはFeature) 境界付けられたコンテキストでは、Optionオブジェクトはありません。これは奇妙に見え、最初は間違っているように見えるかもしれませんが、そのコンテキストの Option オブジェクトは、そのコンテキストに付加価値を提供しません。説明を保持するだけでは、クラスを定義するのに十分ではありません。

ただし、私の考えでは、 OptionIdentity には整数ではなく説明が含まれている必要があります。なんで?整数はドメインの専門家には何も言わないからです。「OS:102」は誰にとっても意味がありませんが、「OS:Debian GNU/Linux」はログ、例外、ブレインストーミングで明示されます。

これは、あなたの例の用語をよりビジネス指向のものに置き換えるのと同じ理由です (optionGroup の代わりに機能、オプションの代わりにソリューション、依存関係の代わりに要件)。ドメインの専門家は、それらを正確に表現するために、しばしば不可解な従来型の新しい言語を設計することを余儀なくされました。アプリケーションを構築するには、それを十分に理解する必要があります

  1. anOptionは値オブジェクトであると述べました。

    この場合_id、タイプのメンバーが呼び出されOptionIdentityます。値オブジェクトは識別 ID を持つことができますか?

これは良い質問です。

アイデンティティとは、変化を気にするときに何かを伝えるために使用するものです。オプションの進化を気にしないコンテキストでは、そこでモデル化したいの
進化だけです。実際、そのコンテキストでは、(またはおそらくより適切な表現で)は、immutable にしたい値です。ProductsManagementProductConfigurationOptionSolution

そのため、Option は値オブジェクトであると言ったのです。そのコンテキストでの「OS:Debian GNU/Linux」の進化については気にしません。その要件が手元の ProductConfiguration によって確実に満たされるようにしたいだけです。

  1. のコードでOptionは、 のコンストラクタidと のリストを使用しdependenciesます。

    Optionはの一部としてのみ存在することを理解していますOptionGroup(型にはtypeOptionIdentityのメンバーが必要であるため)。別の集約インスタンス内にある別のインスタンスへの参照を保持することは許可されていますか? これは、集約ルートへの参照のみを保持し、内部のものを参照しないという DDD 規則に違反していますか?_groupOptionGroupIdentityOptionOptionOptionGroup

いいえ、それが私が共有識別子モデリング パターンを設計した理由です。

  1. 通常、集約ルートとその子エンティティを個別ではなくオブジェクト全体として永続化します。これは、オブジェクト/リスト/辞書を集約ルート内のメンバーとして保持することで行います。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)
}
于 2013-11-18T16:33:56.150 に答える