我々フレームワークにFluentな設定機能を追加する方法
例えばAutoMapperとかね、オブジェクトの変換ルールをこんな風に設定しますが(・∀・)
public class CalendarEvent { public DateTime EventDate { get; set; } } public class CalendarEventForm { public DateTime EventDate { get; set; } public int EventHour { get; set; } public int EventMinute { get; set; } } Mapper.CreateMap<CalendarEvent, CalendarEventForm>() .ForMember( dest => dest.EventDate, opt => opt.MapFrom( src => src.EventDate.Date ) ) .ForMember( dest => dest.EventHour, opt => opt.MapFrom( src => src.EventDate.Hour ) ) .ForMember( dest => dest.EventMinute, opt => opt.MapFrom( src => src.EventDate.Minute ) );
ForMember()で設定とか(・∀・)カコイイよね、っということで、オレオレフレームワークにもFluentな設定機能を追加する方法を考えてみます。
サンプルフレームワーク
っということで、題材としてはFactoryというか簡易コンテナのようなものを例に取ってみて。
まずはFluentな設定機能無しの実装として、以下のような実装を用意してみて(・ω・)
// 生成するTypeに関するメタ情報 public class TypeMeta { // 生成するType public Type Type { get; private set; } // Singletonにするか public bool IsSingleton { get; set; } // コンストラクタをFactory任せではなく独自に行うか public Func<object> CustomConstructer { get; set; } // プロパティメタ情報一覧 private readonly Dictionary<string, PropertyMeta> propertyMetas = new Dictionary<string, PropertyMeta>(); // コンストラクタ public TypeMeta(Type type) { Type = type; foreach( PropertyInfo pi in type.GetProperties( BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance ) ) { PropertyMeta propertyMeta = new PropertyMeta( pi ); this.propertyMetas[ pi.Name ] = propertyMeta; } } // プロパティ一覧取得 public IEnumerable<PropertyMeta> GetProperties() { return this.propertyMetas.Values; } // プロパティ取得 public PropertyMeta GetProperty(string name) { PropertyMeta propertyMeta; this.propertyMetas.TryGetValue( name, out propertyMeta ); return propertyMeta; } }
// Typeのプロパティに関するメタ情報 public class PropertyMeta { // 初期値の設定を独自に行うか public Func<object, object> ValueResolver { get; set; } // 再帰的な生成対象から除外するか public bool Ignore { get; set; } // プロパティの型 public Type PropertyType { get { return this.propertyInfo.PropertyType; } } // PropertyInfo private readonly PropertyInfo propertyInfo; // コンストラクタ public PropertyMeta(PropertyInfo pi) { this.propertyInfo = pi; } // 値の設定 public void SetValue(object target, object value) { this.propertyInfo.SetValue( target, value, null ); } }
// 例題用簡易ファクトリー public static class Factory { // メタ情報一覧 private static readonly Dictionary<Type, TypeMeta> typeMetas = new Dictionary<Type, TypeMeta>(); // シングルトン一覧 private static readonly Dictionary<Type, object> singletons = new Dictionary<Type, object>(); // メタ情報の追加 public static void AddMetaData(TypeMeta typeMeta) { typeMetas[ typeMeta.Type ] = typeMeta; } // インスタンス取得 public static T Create<T>() { return (T)Create( typeof(T) ); } // インスタンス取得 public static object Create(Type type) { // メタ情報が無いインスタンスはそのまま生成 TypeMeta typeMeta; if( !typeMetas.TryGetValue( type, out typeMeta ) ) { return CreateInstance( type ); } // カスタムコンストラクタが定義されていればそれで生成 object target = typeMeta.CustomConstructer != null ? typeMeta.CustomConstructer() : CreateInstance( typeMeta.Type ); // プロパティについて再帰的に生成 foreach( PropertyMeta propertyMeta in typeMeta.GetProperties() ) { if( !propertyMeta.Ignore ) { object value = propertyMeta.ValueResolver != null ? propertyMeta.ValueResolver( target ) : Create( propertyMeta.PropertyType ); propertyMeta.SetValue( target, value ); } } return target; } // インスタンス生成 private static object CreateInstance(Type type) { if( type.IsValueType || ( type == typeof(string) ) ) { return null; } else { TypeMeta typeMeta; if( typeMetas.TryGetValue( type, out typeMeta ) ) { // シングルトン? if( typeMeta.IsSingleton ) { object target; if( !singletons.TryGetValue( type, out target ) ) { target = Activator.CreateInstance( type ); singletons[ type ] = target; } return target; } } return Activator.CreateInstance( type ); } } }
例題用ということで手抜きな実装ですが(´д`;)
マルチスレッドの考慮はしてないですし、メンバの処理もPropertyInfoのみを対象にしています。
あと、interfaceと実装の分離なんかも今回は省略します。
仕様検討
っで、ここまでの実装は既にあるという所からが話の本題(`・ω・´)
現状だと、Factoryクラスのメタ情報の設定は以下の様にに行うわけですが。
// 従来の設定方法 TypeMeta typeMeta = new TypeMeta( typeof(DataObject) ); typeMeta.GetProperty( "Id" ).ValueResolver = src => IdGenerator.Next(); typeMeta.GetProperty( "Ignore" ).Ignore = true; typeMeta.GetProperty( "CreatedAt" ).ValueResolver = src => DateTime.Now; Factory.AddMetaData( typeMeta );
これに以下のような、Type safeかつFluentな設定ができるような処理を追加してみたいと思います。
// Type safeでFluentな設定方法
Factory.CreateMeta<DataObject>().
ForMember( c => c.Id, option => option.UseFrom( src => IdGenerator.Next() ) ).
ForMember( c => c.Ignore, option => option.Ignore() ).
ForMember( c => c.CreatedAt, option => option.UseValue( DateTime.Now ) );
Fluentな設定機能の追加
手順としては、まずはメタ情報の式表現として以下のようなクラスを用意して。
// TypeMetaの式表現 public class TypeMetaExpression<TTarget> { private readonly TypeMeta typeMeta; // コンストラクタ public TypeMetaExpression(TypeMeta typeMeta) { this.typeMeta = typeMeta; } // シングルトン設定 public TypeMetaExpression<TTarget> UseSingleton() { this.typeMeta.IsSingleton = true; return this; } // カスタムコンストラクタ設定 public TypeMetaExpression<TTarget> CreateUsing(Func<TTarget> ctor) { this.typeMeta.CustomConstructer = () => ctor(); return this; } // メンバPropertyMetaの設定 public TypeMetaExpression<TTarget> ForMember(Expression<Func<TTarget, object>> expr, Action<PropertyMetaExpression<TTarget>> option) { string name = ExpressionUtil.PropertyName<TTarget>( expr ); PropertyMeta propertyMeta = this.typeMeta.GetProperty( name ); option( new PropertyMetaExpression<TTarget>( propertyMeta ) ); return this; } }
// PropertyMetaの式表現 public class PropertyMetaExpression<TTarget> { private readonly PropertyMeta propertyMeta; // コンストラクタ public PropertyMetaExpression(PropertyMeta propertyMeta) { this.propertyMeta = propertyMeta; } // 除外設定 public void Ignore() { this.propertyMeta.Ignore = true; } // カスタム値設定(Func) public void UseFrom<TMember>(Func<TTarget, TMember> resolver) { this.propertyMeta.ValueResolver = src => resolver( (TTarget)src ); } // カスタム値設定(固定値) public void UseValue<TMember>(TMember value) { this.propertyMeta.ValueResolver = src => value; } }
TypeMetaExpressionの各メソッドでは、Fluentな処理にするためにthisをreturnしています。
また、ForMemberメソッドではExpressionによりType safeにプロパティ名を取得しています。
Expressionからプロパティ名を取得するユーティリティーはこんな感じで(・ω・)
// Expression関連のユーティリティー public static class ExpressionUtil { // プロパティ名取得 public static string PropertyName<TTarget>(Expression<Func<TTarget, object>> expr) { PropertyInfo pi = GetMemberInfo( expr ) as PropertyInfo; if( pi == null ) { throw new ArgumentException( "expr" ); } return pi.Name; } // ExpressionからMemberInfo取得 public static MemberInfo GetMemberInfo(Expression expr) { while( true ) { switch( expr.NodeType ) { case ExpressionType.Convert: expr = ((UnaryExpression)expr).Operand; break; case ExpressionType.Lambda: expr = ((LambdaExpression)expr).Body; break; case ExpressionType.MemberAccess: MemberExpression memberExpression = (MemberExpression)expr; if( memberExpression.Expression.NodeType != ExpressionType.Parameter && memberExpression.Expression.NodeType != ExpressionType.Convert ) { throw new ArgumentException( "expr" ); } MemberInfo member = memberExpression.Member; return member; default: return null; } } } }
っで、最後にFactoryにTypeMetaExpressionを返すメソッドを追加して終了です。
public static class Factory { ... // TypeMetaの式表現を返す public static TypeMetaExpression<TTarget> CreateMeta<TTarget>() { TypeMeta typeMeta = new TypeMeta( typeof(TTarget) ); AddMetaData( typeMeta ); return new TypeMetaExpression<TTarget>( typeMeta ); } }
テスト
Type safeでFluentな設定ができるようになったので、その動作を確認してみます。
// Singletonテスト用クラス public class SingletonObject { } // カスタムコンストラクタテスト用クラス public class CustomObject { public string Data { get; set; } } // Ignoreテスト用クラス public class IgnoreObject { } // テスト用クラス public class DataObject { public int Id { get; set; } public IgnoreObject NonIgnore { get; set; } public IgnoreObject Ignore { get; set; } public CustomObject Custom { get; set; } public SingletonObject Singleton { get; set; } public DateTime CreatedAt { get; set; } public string Name { get; set; } public int Amount { get; set; } } // ID生成用ヘルパー public static class IdGenerator { private static int current = 1; public static int Next() { return current++; } }
public void Example() { // Singleton設定 Factory.CreateMeta<SingletonObject>(). UseSingleton(); // カスタムコンストラクタ設定 Factory.CreateMeta<CustomObject>(). CreateUsing( () => new CustomObject() { Data = "Created" } ). ForMember( c => c.Data, option => option.Ignore() ); // IdはIdGeneratorで初期値を設定 // NonIgnoreは再帰的に生成、Ignoreは生成を除外 // CreatedAtはDateTime.Nowを初期値で設定 Factory.CreateMeta<DataObject>(). ForMember( c => c.Id, option => option.UseFrom( src => IdGenerator.Next() ) ). ForMember( c => c.Ignore, option => option.Ignore() ). ForMember( c => c.CreatedAt, option => option.UseValue( DateTime.Now ) ); // インスタンス生成 DataObject obj1 = Factory.Create<DataObject>(); Debug.Assert( obj1.Id == 1 ); Debug.Assert( obj1.Custom.Data == "Created" ); Debug.Assert( obj1.NonIgnore != null ); Debug.Assert( obj1.Ignore == null ); Debug.Assert( obj1.Name == null ); Debug.Assert( obj1.Amount == 0 ); // インスタンス生成 DataObject obj2 = Factory.Create<DataObject>(); Debug.Assert( obj2.Id == 2 ); Debug.Assert( !ReferenceEquals( obj1, obj2 ) ); Debug.Assert( ReferenceEquals( obj1.Singleton, obj2.Singleton ) ); Debug.Assert( !ReferenceEquals( obj1.NonIgnore, obj2.NonIgnore ) ); }
っということで、Type safeでFluentな設定機能の追加ができました(・∀・)
今回のネタ元はAutoMapperのソースで。
フレームワーク的なものを作っていると、そこで遭遇する固有のパターンみたいなものがいくつかありますが。
こういうパターン(式表現クラスを使ってType safeでFluentな設定機能の追加する)にも名称が欲しい所(・ω・)