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

前回まででドメインを利用するコードをかなりリッチにできました。ただ、サブルーチン的な構造化を機械的に行ってきたため結合度が低くく凝集度が高いコードにはなっていません。

たとえば在庫判定処理。引数に予約を含めているのですがこれはかなり座りが悪くなっています。もともと予約が別の処理で利用していたためですが、共通化した処理でこのような前提はリーズナブルではありません。

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;
}

ここは該当会員にとっての貸出可能な在庫があるかという意図で予約のかわりに会員を引数に指定することでより使いやすくなります。(名前もHasRentableStockにしたほうが良いかもしれませんが)

public static bool HasStock(this Title title, Member member)
{
    var stock = title.TitleStock;
    var reserve = context.Reserve.SingleOrDefault(p => p.TitleNo == stock.TitleNo
        && p.MemberNo == member.MemberNo && p.Status == "予約中");

    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;
}

このような小さな改訂を進めるとより再利用しやすくなります。

また、O/Rマッパーの機能をもう少しだけ利用して、テーブル間のリレーションを利用したナビゲーションができるようにすると、テーブルの読み出し処理を省略できて、最終的なコードは以下のようになりました。

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("延滞しています");
        if (member.IsOverRental()) throw new ApplicationException("貸出数が超えています");

        var title = item.Title;
        if (!title.HasStock(member)) throw new ApplicationException("在庫がありません");
        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;

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

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

元のコードに比べてみていかがでしょうか?
かなり読みやすくなったように感じます。

また、いろいろ手順を踏んでコードを改訂してきましたが、結局のところ

  1. 通化したいコードを識別して
  2. 処理の所有者であるエンティティを決定(処理に必要なデータを持っているエンティティ)
  3. 拡張メソッドで実装

するだけなので、トランザクションスクリプトで構造化ができるチームであれば難なく導入できると思っています。
ボトムアップでのドメインモデル実装なので設計モデルが無くても導入できる点も敷居の低くしてくれています。