ドメインモデル駆動開発をやってみる(3)
リポジトリ
リポジトリをモデルに導入します。まずはインタフェースを定義します。最終的にはデータベースから読み出しリポジトリの作成になりますが、今回はデータベースの導入をできるだけ遅延して、後で実装クラスを入れ替えるようにしておきます。まずは、テスト用のオブジェクトのリストをもつリポジトリを作成します。
public interface IRepository{ List Find(Expression > query); }
public class EmployeeRepository : IRepository{ public List Find(Expression > query) { return objectRepository.Where(query.Compile()).ToList(); } private List objectRepository = new List () { new Employee() { EmployeeNo="001", Name = "user1", Section="Sales", CellularPhone="090-999-9991", OfficePhone = "06-6666-6661" }, new Employee() { EmployeeNo="002", Name = "user2", Section="consulting", CellularPhone="090-999-9992", OfficePhone = "06-6666-6662" }, new Employee() { EmployeeNo="003", Name = "user3", Section="Sales", OfficePhone = "06-6666-6663" }, new Employee() { EmployeeNo="004", Name = "user4", Section="consulting", CellularPhone="090-999-9994" }, }; }
モデル実行エンジン
リポジトリを入れ替え可能にする仕組みはいくつかあります。いまではDIを使うのが一般的のようです。今回も同じような仕組みなのですが、もう少し具象化しモデルを実行するためエンジンを作成しリポジトリをサービスとして取り扱うようにします。マイクロカーネル・パターンに近い考え方です。
ModelRuntimeがモデル実行エンジンでリポジトリをサービスとして登録すると実装しているインタフェースでサービスを取得することができる仕組みを用意しています。
public void MyTestInitialize() { Runtime = new ModelRuntime(); Runtime.AddService(new ObjectRepository.EmployeeRepository()); Runtime.AddService(new ObjectRepository.CustomerRepository()); } [TestMethod] public void TestEmployeeList() { var repository = Runtime.GetService>(); Assert.AreEqual("001", repository.Find(p => p.Name == "user1").Single().EmployeeNo); }
リポジトリの拡張
リポジトリには汎用的なFindメソッドだけを用意したのですが、Idで1つのエンティティを検索することはよくあります。ただ、IDは個々のエンティティで型やプロパティ名が変化するため共通化することが案外難しいもので、個別に記述が必要になります。
public class EmployeeRepository : IRepository{ ... public Employee FindById(string id) { return objectRepository.Where(p => p.EmployeeNo == id).Single(); } ... } [TestMethod] public void TestEmployee() { Assert.AreEqual("user1", Runtime.EmployeeRepository.FindById("001").Name); }
共通化にはidをobject型にしたメソッドをインタフェースに定義するアイデアもありますが、すべてのエンティティで実装が必要で、同じような処理を何度も記述しなくてはならないのは避けたいものです。
エンティティによっては実装すら必要ないケースも考えられるので、少なくともこのようなケースだけでも上手く対応できる方法を考えてみます。C#3.0で導入されたpartialメソッドは実装があれば呼び出されというものですが、リフレクションを利用するとこの判断が実装時に実施することができます。このアイデアと拡張メソッドと組み合わせて対象クラスが該当のメソッドがあればそのメソッドを実行するという汎用メソッドを作成することが可能になります。
public static class RepositoryExtender { public static TEntity FindById( this IRepository repository, TId id) { MethodInfo findMethod = repository.GetType().GetMethod("FindById"); if (findMethod == null) throw new NotImplementedException(); return (TEntity)findMethod.Invoke(repository, new object[] { id }); } } [TestMethod] public void TestCutomer() { Assert.AreEqual("Customer1", Runtime.CustomerRepository.FindById("1").Name); }
メタプログラミング
拡張メソッドとリフレクションを組み合わせは強力です。リフレクションを利用して型の情報を取得してメタプログラミングが可能になります。たとえば、Idというプロパティを型がもっていればIdを使った検索用のExpression Treeを作成して検索処理を実行するということが可能になります。これであればIdを持っているエンティティに対してはデフォルトの実装を提供できるようになります。さらに属性と組み合わさればさらに強力なメタプログラミングが可能になります。
public static class RepositoryExtender { public static TEntity FindById( this IRepository repository, TId id) { MethodInfo findMethod = repository.GetType().GetMethod("FindById"); if (findMethod != null) return (TEntity)findMethod.Invoke(repository, new object[] { id }); PropertyInfo idProp = typeof(TEntity).GetProperty("Id"); if (idProp == null) throw new NotImplementedException(); return repository.Find(BuildEqQueryExpression (id, idProp)) .SingleOrDefault(); } private static Expression > BuildEqQueryExpression (TId id, PropertyInfo idProp) { var para = Expression.Parameter(typeof(TEntity), "p"); var prop = Expression.Property(para, idProp); var exp = Expression.Lambda >( Expression.Equal(prop, Expression.Constant(id)), para); return exp; } }