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

データベースを定義せずにドメインモデルを利用して開発を進めてきました。画面も含めて特に問題なく開発することはできました。作成されたドメインモデルはこんな感じになっています。

[Serializable]
public partial class Message
    : IValidator
{
    public int Id { get; set; }
    public DateTime ReceiptDateTime { get; set; }
    public string Contents { get; set; }
    public ResponseAction Action { get; set; }
    public Employee ReceiptPerson { get; set; }
    public Employee TargetPerson { get; set; }
    public Caller CallerPerson { get; set; }
    public string CallBackPhoneNo { get; set; }

    public bool HasCallablePhoneNo()
    {
        if (!string.IsNullOrEmpty(CallBackPhoneNo)) return true;
        var callableList = CallerPerson.ListCallablePhoneNo();
        if (callableList.Count > 0) return true;

        return false;
    }

    public List GetErrors()
    {
        List errors = new List();
        if (Action == ResponseAction.CallBack && !HasCallablePhoneNo()) 
                errors.Add(new ApplicationError("ErrorNoCallablePhoneNo"));
        errors.AddRange(CallerPerson.GetErrors());
        return errors;
    }
}

[Serializable]
public abstract class Caller
    : IValidator
{
    public Caller(string callerPhoneNo)
    {
        this._callerPhoneNo = callerPhoneNo;
    }

    public abstract string Section { get; }
    public abstract string Name { get; }
    public string CallerPhoneNo
    {
        get { return _callerPhoneNo; }
    }
    private string _callerPhoneNo;

    public virtual List ListCallablePhoneNo()
    {
        return new List();
    }

    public virtual List GetErrors()
    {
        List errors = new List();
        if (string.IsNullOrEmpty(this.Name)) errors.Add(new ApplicationError("ErrorMustSetName"));
        return errors;
    }
}
...


データベースの作成

データベースを作成する方法ですが、LINQ to SQLでデータベースを作成する機能が用意されています。マッピング情報から自動的にデータベースを作成してくれる機能のようです。

[TestMethod]
public void TestCreateDatabase()
{
    var db = new MessageDataContext(@"c:\TempData\TempTelephoneMessage.sdf");
    if (db.DatabaseExists()) db.DeleteDatabase();
    db.CreateDatabase();
}


マッピング情報の定義

マッピング情報の指定は、XMLで指定する方法とマッピングするクラスの属性を指定する2つの方法が予め用意されています。XMLファイルを記述するのがめんどくさかったので取りあえず属性を指定する方法でトライします。まずは、カスタムのDataContextを作成し、テーブルにあたるクラスをTable<>でプロパティとして定義します。

public class MessageDataContext
    : DataContext
{
	private static MappingSource mappingSource = new AttributeMappingSource();

    public MessageDataContext(string connectionString)
        : base(connectionString, mappingSource) 
    { 
    }

    public Table Messages
    {
        get { return this.GetTable(); }
    }

    public Table Employees
    {
        get { return this.GetTable(); }
    }

    public Table Callers
    {
        get { return this.GetTable(); }
    }

    public Table Customers
    {
        get { return this.GetTable(); }
    }
}


テーブル定義

テーブル定義は、テーブルとマッピングするクラスに対してTable属性をつけ、カラムに対してColumn属性をつければ何とかTableを生成してくれます。属性のパラメータを付与すればテーブルカラムに主キーなど特別な属性をつけることができます。この程度の改訂であればドメインモデルへの特別な影響が与えないので特に問題ありません。


[Serializable][Table]
public partial class Message
    : IValidator
{
    [Column(IsPrimaryKey = true)]
    public int Id { get; set; }

    [Column]
    public DateTime ReceiptDateTime { get; set; }
    ...


関連の定義

次に関連の定義ですが、データベース上では参照キーが必要になりますので、このためのカラムをドメインモデルに追加する必要があります。ドメインモデルを変更しないといけないため嫌なのですが、追加したカラムは無視することにしてとりあえず許容することにします。まずはデータベース作成なので実装も最低限にしておきます。

[Serializable][Table]
public partial class Message
    ...

    [Association(ThisKey = "ReceiptPersonEmployeeNo", IsForeignKey = true)]
    public Employee ReceiptPerson { get; set; }
    [Column]
    public string ReceiptPersonEmployeeNo { get; set; }

    [Association(ThisKey = "TargetPersonEmployeeNo", IsForeignKey = true)]
    public Employee TargetPerson { get; set; }
    [Column]
    public string TargetPersonEmployeeNo { get; set; }

    [Association(ThisKey = "Id", IsForeignKey = true)]
    public Caller CallerPerson { get; set; }
    ...


データベース

他のテーブルも同様にマッピング情報を定義しテストコードを実行するとデータベースを作成することができました。


永続化機能の確認

データベースが作成できたので、テストデータをデータベースにロードするシナリオにトライしてみます。

[TestMethod]
public void TestCreateEmployee()
{
    Runtime.EmployeeRepository.Find().ForEach(
        p => Runtime.EmployeeRepository.Remove(p));
    var testData = new ObjectRepository.EmployeeRepository();
    testData.Find().ForEach(p => Runtime.EmployeeRepository.Add(p));
    Runtime.GetService().SubmitChanges();
}


リポジトリの作成

LINQ to SQLにおいてDataContextがリポジトリの位置づけなので、そのまま利用すればいいだけです。

public abstract class BaseRepository
    : IRepository
{
    private DataContext Context { get; set; }

    public BaseRepository(DataContext context)
    {
        this.Context = context;
    }

    public virtual IQueryable GetTable()
    {
        return (IQueryable)Context.GetTable(typeof(TEntity));
    }

    public virtual TEntity Add(TEntity entity)
    {
        Context.GetTable(typeof(TEntity)).Add(entity);
        return entity;
    }
    public virtual void Remove(TEntity entity)
    {
        Context.GetTable(typeof(TEntity)).Remove(entity);
    }
}

public class EmployeeRepository
    : BaseRepository
{
    public EmployeeRepository(DataContext context)
        : base(context)
    {
    }

    public Employee FindById(string id)
    {
        return this.Find(p => p.EmployeeNo == id).Single();
    }
}


永続化サービス

データベースにデータを書き込むタイミングを制御する新しいIPersistService を定義し用意します。将来的にはトランザクションの制御も含めて実装すること予想されますが、現時点では、データベースへのフラッシュ処理を指示する機能だけとり扱います。

public interface IPersistService 
{
    void SubmitChanges();
}

public class PersistService : IPersistService
{
    private DataContext Context { get; set; }
    public PersistService(DataContext db)
    {
        this.Context = db;
    }

    public void SubmitChanges()
    {
        Context.SubmitChanges();
    }  
}


複雑なクラス

単純なパターンの従業員(Employee)クラスは上手くいったので、次は少し複雑な伝言(Message)クラスです。

[TestMethod]
public void TestCreateMessage()
{
    Runtime.MessageRepository.Find().ForEach(
        p => Runtime.MessageRepository.Remove(p));
    var msg = new Message()
    {
        Action = ResponseAction.CallAgain,
        CallBackPhoneNo = "06-666-66666",
        CallerPerson = new InsideCaller(null,
            Runtime.EmployeeRepository.FindById("003")),
        Id = 1,
        ReceiptDateTime = DateTime.Now,
        ReceiptPerson = Runtime.EmployeeRepository.FindById("002"),
        TargetPerson = Runtime.EmployeeRepository.FindById("001"),
    };
    Runtime.MessageRepository.Add(msg);
    Runtime.GetService().SubmitChanges();
}

public class MessageRepository
    : BaseRepository
{
    public MessageRepository(DataContext context)
        : base(context)
    { 
    }
}


イミュータブルにできない

実際にテストしてみると実行エラーが発生します。LINQ to SQLがデフォルトコンストラクタがないとダメなようです。

Test method TelephoneMessage.Test.DBRepositoryTest.TestCreateMessage threw exception: System.MissingMethodException: No parameterless constructor defined for this object..

問題になっている部分はどうも電話主(Caller)クラスでイミュータブル(不変)にするために、必要な情報をコンストラクタで指定し、全てのプロパティをRead Onlyにしています。

[Serializable]
[Table]
[InheritanceMapping(Code = "InsideCaller", Type = typeof(InsideCaller), IsDefault = true)]
[InheritanceMapping(Code = "CustomerCaller", Type = typeof(CustomerCaller))]
[InheritanceMapping(Code = "GeneralCaller", Type = typeof(GeneralCaller))]
public abstract class Caller
    : IValidator
{
    public Caller(string callerPhoneNo)
    {
        this._callerPhoneNo = callerPhoneNo;
    }

    [Column(IsPrimaryKey = true)]
    public int Id { get; set; }

    [Column]
    public abstract string Section { get; }

    [Column]
    public abstract string Name { get; }

    [Column]
    public string CallerPhoneNo 
    {
        get { return _callerPhoneNo; } 
    }
    private string _callerPhoneNo;

    [Column(IsDiscriminator = true)]
    public string Type { get; set; }

    ...

試しにデフォルトのコンストラクタを追加しても、Read Onlyのプロパティでエラーになり、Callerクラスをイミュータブルにすることは難しそうです。

Test method TelephoneMessage.Test.DBRepositoryTest.TestCreateMessage threw exception: System.InvalidOperationException: Unable to assign value to read only property 'System.String Section'..

ドメインモデルへの影響

ドメインモデルをLINQ to SQLで直接利用できるようにいくつかの修正を加えてきました。まとめるとこんな点です。

  • 属性の追加
  • 関連のためのフィールドの追加
  • デフォルトコンストラクタ
  • イミュータブルのミュータブル化

最後のミュータブル化も実務的には可能です。データベースを先に作って LINQ to SQLのdbmlでクラスを作成してやっていけば必然的そうなるので難しい点もないと考えています。ただ、このように作ったドメインモデルは同じような制約を受けることになるため本質的に同じ問題を抱えます。ドメインモデルとデータベースのスキーマは似ているとこともあるのですがやはり違うところも多いので、単純なメタマッピングでは無理でより柔軟な仕組みを考える必要がありそうです。
ということで、残念ながらこのやり方はドメインモデルのクラスにかなりの影響を与えることになるのであきらめることにします。