ドメインモデル貧血症のコードをリッチ化してみる?その3

引き続き、ドメインモデル貧血症のコードを再利用可能そしてドメインモデルをリッチ化することにチャレンジします。
前回、サブルーチン化することで処理の構造化を進め、プログラムが読みやすくなりました。しかし、まだ再利用するためには共通処理として利用しやすい構造、さらにドメインモデルをリッチにするためにはドメインモデルに機能を追加していく必要があります。まずは再利用できるように共通処理として抽出するようにします。

まず、共通処理化する前に、少しAPIの使いやすさをあげておくようにします。各メソッドに渡していたDBアクセス用のコンテキストをメソッドの引数ではなくメンバー変数にします。これによって、各メソッドの呼び出しでいちいち引き渡す必要がなくなります。

private LibraryDB context;

public int Entry(string memberNo, string itemNo)
{
    using (context = new LibraryDB())
    {
                     ...
        if (!HasStock(item, stock, reserve)) 
            throw new ApplicationException("在庫がありません");
                     ...
    }
}

public bool HasStock(Item item, TitleStock stock, Reserve reserve)
{
    if (stock.ItemCount - stock.RentalCount == 0) return false;
    if (stock.ItemCount - stock.RentalCount - stock.ReserveCount <= 0)
    {
        if (reserve == null) return false;
        var priCount = context.Reserve.Where(p => p.TitleNo == item.TitleNo
            && p.Status == "予約中" && p.ReserveDate < reserve.ReserveDate).Count();
        if (stock.ItemCount - stock.RentalCount <= priCount) return false;
    }
    return true;
}

つぎにメソッドを元のクラスから共通クラスに移動させます。共通と個別のクラスを明確に分けるとで依存性の制御がやりやすくなります。

var helper = new RentalServiceHelper(context);
                     ...
if (!helper.HasStock(stock, reserve)) throw new ApplicationException("在庫がありません");

                     ...

public class RentalServiceHelper
{
    private LibraryDB context;

    public RentalServiceHelper(LibraryDB context)
    {
        this.context = context;
    }
                     ...

    public bool HasStock(TitleStock stock, Reserve reserve)
    {
        if (stock.ItemCount - stock.RentalCount == 0) return false;
        if (stock.ItemCount - stock.RentalCount - stock.ReserveCount <= 0)
        {
            if (reserve == null) return false;
            var priCount = context.Reserve.Where(p => p.TitleNo == stock.TitleNo
                && p.Status == "予約中" && p.ReserveDate < reserve.ReserveDate).Count();
            if (stock.ItemCount - stock.RentalCount <= priCount) return false;
        }
        return true;
    }
}

これでRentalServiceHelperを利用して再利用ができるようになりました。ただ、元のプログラムがヘルパー経由で呼び出されるのが少しノイズで可読性が若干下がってしまいました。これから、この点を改訂してより利用し易くします。

どのようにすると利用し易くなるかですが、ここはリッチなドメインモデルにヒントをもとめましょう。リッチなドメインモデルではエンティティに関連する処理がメソッドやプロパティに実装されているので、すぐに発見して利用することができます。では今回も共通化したメソッドをドメインモデルに直接実装していけば良いというアイデアが浮かびますが、共通化したメソッドはDBアクセスの処理を含んでいるのでこのままでは難しい状況です。データマッパーを利用した本格的なドメインモデルのアプローチが必要になります。
それではどのように方法があるかと考えると、C#には直接実装しないでもクラスにメソッドを追加できる拡張メソッドという方法があります。MIX-IN機能ですね。この技術を利用すれば貧血症のドメインモデルに機能を追加してリッチ化し、なおかつ、再利用し易くすることができます。

このための変更は簡単で、元のクラスをstaticクラスにして拡張したいエンティティに対してthisを付与すればOKです。staticクラスになるのでDBアクセスのコンテキストの引き渡しは少し変更が必要です。今回はCallContextというスレッドローカル的なストアを利用して呼び出し側の処理と共有するようにします。

public static class RentalServiceHelper
{

    public static LibraryDB context
    {
        get { return (LibraryDB)CallContext.GetData("Context"); }
        set { CallContext.SetData("Context", value); }
    }
                     ...

    public static bool HasStock(this TitleStock stock, Reserve reserve)
    {
        if (stock.ItemCount - stock.RentalCount == 0) return false;
        if (stock.ItemCount - stock.RentalCount - stock.ReserveCount <= 0)
        {
            if (reserve == null) return false;
            var priCount = context.Reserve.Where(p => p.TitleNo == stock.TitleNo
                && p.Status == "予約中" && p.ReserveDate < reserve.ReserveDate).Count();
            if (stock.ItemCount - stock.RentalCount <= priCount) return false;
        }
        return true;
    }
}

通化するメソッドの引数がIDの場合は、引数の型をエンティティに変更すると拡張できます。

public int IsOverRental(string memberNo)
       ↓
public static bool IsOverdue(this Member member)

この変更の結果、メイン処理は以下のようになります。

public int Entry(string memberNo, string itemNo)
{
    using (context = new LibraryDB())
    {
        context.Log = Console.Out;
        var member = context.Member.SingleOrDefault(p => p.MemberNo == memberNo);
        if (member == null) throw new ApplicationException("該当メンバーがいません");
        var item = context.Item.SingleOrDefault(p => p.ItemNo == itemNo);
        if (itemNo == null) throw new ApplicationException("該当アイテムがありません");

        if (member.IsOverdue())
            throw new ApplicationException("延滞しています");

        var memberType = context.MemberType.Single(p => p.ID == member.MemberTypeID);
        if (member.IsOverRental(memberType))
            throw new ApplicationException("貸出数が超えています");

        var stock = context.TitleStock.Single(p => p.TitleNo == item.TitleNo);
        var reserve = context.Reserve.SingleOrDefault(p => p.TitleNo == item.TitleNo
            && p.MemberNo == member.MemberNo && p.Status == "予約中");

        if (!stock.HasStock(reserve)) 
            throw new ApplicationException("在庫がありません");

        var title = context.Title.Single(p => p.TitleNo == item.TitleNo);
        if (title.IsRecentMagazine()) 
            throw new ApplicationException("最新号の雑誌は貸出できません");

        var rental = new Rental();
        rental.RentalDate = DateTime.Today;
        rental.MemberNo = memberNo;
        context.Rental.InsertOnSubmit(rental);
        context.SubmitChanges(); // rentalNoを取得するため

        var detail = new RentalDetail();
        detail.RentalNo = rental.RentalNo;
        if (reserve != null)
        {
            detail.ReserveNo = reserve.ReserveNo;
            reserve.Status = "貸出済";
        }
        detail.ItemNo = itemNo;
        detail.DueDate = memberType.GetDueDate(title);
        detail.Status = "貸出中";
        detail.SubNo = 1;
        context.RentalDetail.InsertOnSubmit(detail);

        context.SubmitChanges();
        return rental.RentalNo;
    }
}

どうでしょう。リッチなドメインモデルにかなり近づいて来たような感じがしませんか?

最終的には、サブルーチン化のやり方を少し変えた程度で、エンティティ側には何も記述していません(エンティティは貧血症のままです)。処理的にもトランザクションスクリプト処理と同じ動きですが、コードが読みやすく、再利用性も高くなった感じがしないでしょうか?