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

ドメインモデル貧血症はダメなのか?でトップヘビーな設計なしで実装できるけれど、再利用性の割り切りは必要と考えていました。
しかし、これって本当なのか、再利用性のある貧血症のドメインモデルってできないのかなと疑問が湧き上がってきました。そこで、貧血症のドメインモデルを作って、再利用性を高めてみようと思います。

まずはベースになるトランザクションスクリプト的な貧血症ドメインモデルのコードを作成してみます。
題材は図書館での貸し出し処理で、今回のORMはLINQ to SQLを利用することにします。

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 (context.RentalDetail.Any(
            p => p.Rental.MemberNo == memberNo
                && p.Status == "貸出中" && p.DueDate < DateTime.Today))
            throw new ApplicationException("延滞しています");

        var memberType = context.MemberType.Single(p => p.ID == member.MemberTypeID);
        if (context.RentalDetail.Where(
            p => p.Rental.MemberNo == memberNo
                && p.Status == "貸出中").Count() > memberType.MaxRentalCount)
            throw new ApplicationException("貸出数が超えています");

        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("在庫がありません");
        }

        var title = context.Title.Single(p => p.TitleNo == item.TitleNo);
        if (title.TitleTypeID == "2") //雑誌の場合は最新版チェック
        {
            var magazineTitleNo = title.TitleNo.Substring(0,title.TitleNo.IndexOf("-")+1);
            if (context.Title.Where(p => p.TitleNo.StartsWith(magazineTitleNo)
                && p.MagazineNo > title.MagazineNo).Count() == 0) // 最新号判定
            {
                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;
        var rule = context.RentalTermRule.Single(
            p=>p.TitleTypeID == title.TitleTypeID && p.MemberTypeID == memberType.ID);
        detail.DueDate = DateTime.Today.AddDays(rule.Term);
        detail.Status = "貸出中";
        detail.SubNo = 1;
        context.RentalDetail.InsertOnSubmit(detail);

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

それ程複雑なロジックではありませんが、1画面では収まらない量になってしまいました。コードは直観的で書き易く、SQL文を明示するトランザクションスクリプトであればより多くのコードがいることを考えると簡潔なコードと言えなくもありません。

コードの内容はほぼSQL文を明示している方式と変わりはありません。以下の箇所が延滞チェックで、すこし冗長なSQL文ですがJOINを利用したスペシフィックなSQLで実装されています。SQL文を明示していませんが、実行されるSQLは通常のトランザクションスクリプトと同じレベルになっています。

        if (context.RentalDetail.Any(
            p => p.Rental.MemberNo == memberNo
                && p.Status == "貸出中" && p.DueDate < DateTime.Today))
            throw new ApplicationException("延滞しています");

実行されるSQL

SELECT 
    (CASE 
        WHEN EXISTS(
            SELECT NULL AS [EMPTY]
            FROM [RentalDetail] AS [t0]
            INNER JOIN [Rental] AS [t1] ON [t1].[RentalNo] = [t0].[RentalNo]
            WHERE ([t1].[MemberNo] = @p0) AND ([t0].[Status] = @p1) AND ([t0].[DueDate] < @p2)
            ) THEN 1
        ELSE 0
     END) AS [value]