ドメインモデル駆動開発をやってみる(7)

前回ドメインモデルにLINQ to SQLのメタ情報を付与して永続化の仕組みを構築しようと考えましたが、LINQ to SQLが要求するいくつかのインタフェースを実装するためにドメインモデルをかなり変更する必要があり断念しました。
今回は違う方法でチャレンジします。

ドメインモデルとORMクラスの分離

ドメインモデルとデータベースのテーブルの構造はやはり違うものだと考えて完全に分けて実装してみます。方式はドメインモデルとLINQ to SQLで生成されたクラスを相互変換するためのデータマッパーを用意しリポジトリで自動的に変換するようにします。

public class EmployeeRepository
    : BaseRepository
{
    public EmployeeRepository(DataContext context)
        : base(context)
    {
    }
    ...
    protected override TelephoneMessage.Model.Employee ConvertDBToEntity(Employee dbEntity)
    {
        return new TelephoneMessage.Model.Employee()
        {
            EmployeeNo = dbEntity.EmployeeNo,
            Name = dbEntity.Name,
            Section = dbEntity.Section,
            OfficePhone = dbEntity.OfficePhone,
            CellularPhone = dbEntity.CellularPhone,
        };
    }

    protected override Employee ConvertEntityToDB(TelephoneMessage.Model.Employee entity)
    {
        return new Employee()
        {
            EmployeeNo = entity.EmployeeNo,
            Name = entity.Name,
            Section = entity.Section,
            OfficePhone = entity.OfficePhone,
            CellularPhone = entity.CellularPhone,
        };
    }

    protected override Employee FindDBEntity(TelephoneMessage.Model.Employee entity)
    {
        return Context.GetTable()
            .Single(p => p.EmployeeNo == entity.EmployeeNo);
    }
}


ドメインモデルの変更はいらない

前回問題になったイミュータブルなクラスも変更なしにマッピングできます。もちろん、ドメインモデルの変更は全く必要ありません。

public class MessageRepository
    : BaseRepository
{
    ...
    protected override TelephoneMessage.Model.Message ConvertDBToEntity(Message dbEntity)
    {
        var empRep = new EmployeeRepository(Context);
        return new TelephoneMessage.Model.Message()
            {
                Id = dbEntity.Id,
                Action = (Model.ResponseAction)dbEntity.Action,
                CallBackPhoneNo = dbEntity.CallBackPhoneNo,
                Contents = dbEntity.Contents,
                ReceiptDateTime = dbEntity.ReceiptDateTime,
                ReceiptPerson = empRep.FindById(dbEntity.ReceiptPersonEmployeeNo),
                TargetPerson = empRep.FindById(dbEntity.TargetPersonEmployeeNo),
                CallerPerson = FindCaller(dbEntity.Id),

            };
    }

    protected Caller ConvertDBCaller(Model.Message msg, Model.Caller ent)
    {
        if (ent is Model.InsideCaller)
        {
            var inside = (Model.InsideCaller)ent;
            return new InsideCaller() {
                Name = inside.Name,
                Id = msg.Id,
                Section = inside.Section,
                CallerPhoneNo = inside.CallerPhoneNo,
                CallerEmployeeNo = inside.CallerEmployee.EmployeeNo,
            };
        }
        if (ent is Model.CustomerCaller)
        {
            var cust = (Model.CustomerCaller)ent;
            return new CustomerCaller()
            {
                Name = cust.Name,
                Id = msg.Id,
                Section = cust.Section,
                CallerPhoneNo = cust.CallerPhoneNo,
                CallerCustomerNo = cust.CallerCustomer.Id,
            };
        }
        if (ent is Model.GeneralCaller)
        {
            var gene = (Model.GeneralCaller)ent;
            return new GeneralCaller()
            {
                Name = gene.Name,
                Id = msg.Id,
                Section = gene.Section,
                CallerPhoneNo = gene.CallerPhoneNo,
            };
        }
        return null;
    }

    protected override Message ConvertEntityToDB(TelephoneMessage.Model.Message entity)
    {
        return new Message()
        {
            Id = entity.Id,
            Action = (int)entity.Action,
            CallBackPhoneNo = entity.CallBackPhoneNo,
            Contents = entity.Contents,
            ReceiptDateTime = entity.ReceiptDateTime,
            ReceiptPersonEmployeeNo = entity.ReceiptPerson.EmployeeNo,
            TargetPersonEmployeeNo = entity.TargetPerson.EmployeeNo,
            Caller = ConvertDBCaller(entity, entity.CallerPerson),
        };
    }
    ...
}


オブジェクトキャッシュ機能

エンティティの一元管理やダーティチェックを行うための仕組みとしてオブジェクトキャッシュ機能を作成します。今回は簡易的にプロパティ設定処理(set_xxxxメソッド)が呼ばれた場合にダーティ状態をONにします。これでプロパティが変更されたエンティティを自動的にデータベースに書き戻すことができるようになります。

public abstract class CacheableObject
    : AspectObject, ICacheable
{
    ...
    public abstract string Oid { get; }

    public override void AfterConstructorCall(IMethodReturnMessage msg)
    {
    }

    public override void AfterMethodCall(IMethodReturnMessage msg)
    {
        if (msg.MethodName.StartsWith("set_"))
        {
            _isDirty = true;
        }
    }

    public bool IsDirty
    {
        get { return _isDirty; }
    }

    public void AddCache()
    {
        _isDirty = false;
        CacheList[this.Oid]=this;
    }

    public void SubmitChange()
    {
        _isDirty = false;
    }
}


ドメインモデルの改訂

このオブジェクトキャッシュ機能をドメインモデルで利用できるようします。ここで残念ながらドメインモデルを少しだけ変更する必要があります。ただ変更は簡単なもので、パーシャルクラスを利用すると元のソースとは別のソースファイルで完全に分離することも可能です。

partial class Employee
    : CacheableObject
{
    public override string Oid
    {
        get { return this.GetType().FullName + EmployeeNo; }
    }
}

partial class Message
    : CacheableObject
{
    public override string Oid
    {
        get { return this.GetType().FullName + Id; }
    }
}


振り返り

ドメインモデルをほぼ変更なしでデータベースの永続化することは可能であることが確認できました。ただLINQ to SQLを使う場合に暗黙的な要求されるインタフェースや決まりがあり、あらゆるドメインモデルを簡単にマッピング出来るわけはなく、場合によってはドメインモデルの大幅な変更が必要になることもあります。このため、LINQ to SQLで簡単に作成することを考えている場合は、やはり概念・分析モデル -> データベース -> ドメインモデルの順で作成するのがリーズナブルと考えます。より堅牢なドメインモデルを作成したい場合はデータマッピングやオブジェクトキャッシュ機能などを用意した独自のフレームワークの作成が必要になります。今回 Expression Treeの変換など一部難度の高い機能の実装を避け別の仕組みで代用していますが、実務で使う場合はこのあたりも含めて実装する必要がありタフな実装になるかもしれません。このあたり ADO.NETのEntity Frameworkがより高度なマッピングを用意しているようなので期待したいところです。