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

前回の続きです。まずは構造化的な発想で意味のある処理の塊を抜き出してみます。

貸出処理の以下の部分については、「在庫がありません」の例外を返す部分で、3つのバリエーションがありますが、在庫判定処理として抽出できそうです。

var stock = context.TitleStock.Single(p => p.TitleNo == item.TitleNo);
if (stock.ItemCount - stock.RentalCount == 0)
    throw new ApplicationException("在庫がありません");
var reserve = context.Reserve.SingleOrDefault(p => p.TitleNo == item.TitleNo
    && p.MemberNo == member.MemberNo && p.Status == "予約中");
if (stock.ItemCount - stock.RentalCount - stock.ReserveCount <= 0)
{
    if (reserve == null) throw new ApplicationException("在庫がありません");
    var priCount = context.Reserve.Where(p => p.TitleNo == item.TitleNo
        && p.Status == "予約中" && p.ReserveDate < reserve.ReserveDate).Count();
    if (stock.ItemCount - stock.RentalCount <= priCount)
        throw new ApplicationException("在庫がありません");
}

再利用可能なデータをアクセスしている部分を除いて、ほぼそのまま抜き出してみましょう。少し処理順を入れ替えてリファクタリングツールでメソッドの抽出をしました。

public void CheckStock(LibraryDB context, Item item, TitleStock stock, Reserve reserve)
{
    if (stock.ItemCount - stock.RentalCount == 0)
        throw new ApplicationException("在庫がありません");
    if (stock.ItemCount - stock.RentalCount - stock.ReserveCount <= 0)
    {
        if (reserve == null) throw new ApplicationException("在庫がありません");
        var priCount = context.Reserve.Where(p => p.TitleNo == item.TitleNo
            && p.Status == "予約中" && p.ReserveDate < reserve.ReserveDate).Count();
        if (stock.ItemCount - stock.RentalCount <= priCount)
            throw new ApplicationException("在庫がありません");
    }
}

これでエラーチェック処理として再利用できそうです。でも、単に在庫があるかの判定には使えないのでもう少し工夫しましょう。エラー処理をエラー判定部とエラーの通知部に分けて考えます。そうすると以下のようになります。

public bool HasStock(LibraryDB context, 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;
}

名前を分かり易く変え、例外をスローしている部分をreturn文に変えただけですが、これで汎用的な在庫判定処理として利用できます。

同様に延滞、貸出数超過、最新雑誌判定、貸出期限の意味のある処理で構造化すると元のプログラムのメイン部分は以下のようになりました。かなり読みやすくなりました。*1

public int Entry(string memberNo, string itemNo)
{
    using (var 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 (IsOverdue(context, memberNo))
            throw new ApplicationException("延滞しています");

        var memberType = context.MemberType.Single(p => p.ID == member.MemberTypeID);
        if (IsOverRental(context, memberNo, 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 (!HasStock(context, item, stock, reserve)) 
            throw new ApplicationException("在庫がありません");

        var title = context.Title.Single(p => p.TitleNo == item.TitleNo);
        if (IsRecentMagazine(context, title)) 
            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 = GetDueDate(context, memberType, title);
        detail.Status = "貸出中";
        detail.SubNo = 1;
        context.RentalDetail.InsertOnSubmit(detail);

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

でも、再利用するためには、もう少し工夫したいところです。

*1:構造化設計でよくあるパターンで前処理・メイン・後処理に分離することがありますが、それでは当たり前ですが再利用性は上がりません。