ドメインモデルをLINQで構築する(その2)

商品(Product)ドメインモデル

今回は商品(Product)のドメインモデルを作成します。まずは、前回と同様にLINQ to SQLでアクセスするためのdbmlファイルを作成します。今回は複数のテーブルから構成される点や再帰構造や継承構造があるので前回よりも複雑になることが予想されます。

まずは、関連テーブルをDrag&Dropします。自動的に関連が設定されナビゲーションできるような構造になります。

次にProductはいくつかのバリエーションがあるのでグループ商品(GroupProduct)や単体販売商品(SalePuroduct)、付属商品(AttachedProduct)をサブクラスとして定義します。

継承のマッピングSingle Table Inheritanceパターンがサポートされており、以下のような特定のカラムの値によって派生クラスを指定することが可能です。

サブクラス化に伴いProductGroupやProductItemへの関連もサブクラス側の設定できるとよいのですが、dbmlの制約上でできないためベースクラスのProductに残したままにしておきます。

お買い上げオプションのテストケース作成

ここで、商品のドメインモデルの利用シナリオについて考えてみます。お買い上げシナリオで商品は購入されます。このシナリオにおいてお買い上げする商品にどのようなオプションがあるのかユーザは知る必要があります。このため、商品(Product)クラスはオプション情報を提供する必要があります。

この処理は少し複雑なので目標となるテストケースを先に記述して実装することします。

OptionListはオプションアイテムの名称(Main,Sub,Drinkなどオプションの集まり)、追加価格、あと選択はできなけど固定的なアイテムも含めるほうが一般化できるので、それにともない固定かどうかの判断する項目を持つようにさせます。

とりあえずコンパイラを通すためにProductOptionクラスとOptionListプロパティを定義するようにします。

public struct ProductOption
{
    public string ItemName { get; private set; }
    public Product Option { get; private set; }
    public decimal AdditionalPrice { get; private set; }
    public bool IsFixed { get; private set; }
    public string Title
    {
        get { return Option.Title; }
    }

    public ProductOption(string item, Product option,
        decimal additionalPrice, bool isFixed) : this()
    {
        this.ItemName = item;
        this.Option = option;
        this.AdditionalPrice = additionalPrice;
        this.IsFixed = isFixed;
    }
}
partial class SaleProduct
{
    public List OptionList
    {
        get
        {
            return null;
        }
    }
}

あとは、先ほどのテストが通るようにOptionListを作成していきます。

Aggregateパターンの適用

まず詳細の実装の前作業として、ProductItemやProductItemOption、ProductGroupはProductの内部エンティティとして扱うようにします。これによって、外部からの取扱いがシンプルになり複雑さをコントロールし易くなります。DDDでいうところのAggregateパターンにあたります。この実装はdbmlのエンティティクラスのAccessをInternal、また該当クラスへのリレーションを合わせてInternalにすることで可能です。これで外部からはProductとその派生クラスしか直接アクセスできない構造になります。
さらにPrductとProductGroupの関連のロール名を親子関係が分かるような名称に変更します。



ドメインモデルOptionListプロパティの実装

dbmlから生成されたクラスに対してpartialクラスで実装していきます。このときクラスに対するPublicなどの可視性は付与しないでおけばdbmlで可視性を変更してもpartialクラスの可視性を変更せずにすみます。

partial class SaleProduct
{
    public List OptionList
    {
        get
        {
            if (_optionList == null)
            {
                _optionList = new List();
                foreach (var item in this.ProductItems)
                {
                    _optionList.AddRange(item.GetOptionList());
                }
            }
            return _optionList;
        }
    }
    private List _optionList;
}

partial class ProductItem
{
    public bool IsFixed
    {
        get
        {
            if (this.ProductItemOptions.Count() != 1) return false;
            return !(this.ProductItemOptions[0].Product is GroupProduct);
        }
    }

    public List GetOptionList()
    {
        var optionList = new List();
        foreach (var opt in this.ProductItemOptions)
            optionList.AddRange(
                opt.GetOptionProducts().Select(o =>
                    new ProductOption(this.Name, o, opt.AdditionalPrice, this.IsFixed)));
        return optionList;
    }
}

partial class ProductItemOption
{
    public List GetOptionProducts()
    {
        return this.Product.GetLeafProduct();
    }

}

partial class Product
{
    internal virtual List GetLeafProduct()
    {
        return new List() { this };
    }
}

partial class GroupProduct
{
    internal override List GetLeafProduct()
    {
        var list = new List();
        foreach (var child in this.Children)
            list.AddRange(child.Child.GetLeafProduct());
        return list;
    }
}

GetLeafProductはグループの総称でない商品を取得するメソッドなのですがもう少し良い名前に変えたほうが良いかも、とりあえずこのままでまた考えます。

商品(Product)ドメインモデル クラス図

Visual Studio付属のツールを使って現時点のクラス図を作成します。UMLのクラス図とは若干表記法が異なる(コレクションの関連が−−>>で表現されるなど)ので好みが分かれるのですが、実際のコードと同期してくれる点はやはり非常に便利です。また、(未確認ですが)自動生成されたプロパティやメソッドがグレーアウトして表示されるようなので、自動生成を多用している場合にも便利です。