パート2:PropertyUnionの実装の自動化
単純なContactPropertyUnion
クラスを手作業で記述してもかまいませんが、そうすると次のような問題が出てきます。
- 継承階層を変更するときに、
PropertyUnion
クラスにも同等の変更を加える必要がある - 型判定をもっと複雑な継承階層と組み合わせる場合は、バグの原因になりやすい
- そもそも
PropertyUnion
クラスを記述しなければならないことが手間である。これはごく単純な実装で、必要なのは、ラップする継承階層といくつかのメタデータをビルド時に型情報としてコンパイラに渡すことだけである
そこで今度は、.NETのCodeDOM
名前空間を使用してPropertyUnionBuider
を実装する方法を見ていきたいと思います。CodeDOM
とは、言語に依存しないコード記述コードを書くためのライブラリです。
ただし、最初に注意しておきたいことがあります。CodeDOM
を使用すると、コードを記述するコードを作成できます。さらに、このコード記述コードでは任意の.NET言語のコードを記述できます。その点だけを見れば、最初は「素晴らしい!」と思うかもしれません。しかし、素晴らしいことばかりではありません。実際には、CodeDOM
名前空間にはいくつか面倒な点があります。
CodeDOM
は言語固有のイディオムをサポートしていない:CodeDOM
名前空間はすべての.NET言語の最小公分母を表しているので、as
やis
といったC#の便利なキーワードを直接使用できません。その代わりにフレームワークメソッドを使用する必要があります。そのためコードが冗長化し、読みやすさが低下します。CodeDOM
は冗長である:CodeDOM
を使ってアルゴリズムを記述すると、いずれかの.NET言語を使って直接記述したときよりもコードの行数が大幅に増えます。私の場合は、少なくとも3:1程度の比率になります。さらに、CodeDOM
を使用すると1行のコードも長くなります。CodePropertySetValueReferenceExpression
のような長い名前を持つクラスが数多くあるからです。1行の長さが150文字に及ぶことも少なくないので、これらのクラスをある程度使い慣れた後でも、それぞれのコードブロック内で何をしているかを判読するのは容易なことではありません。CodeDOM
は記述しにくい:これが一番の問題です。MicrosoftはCodeDOM
の使い勝手を良くするために尽力しましたが、この問題はいまだに解消されていません。コード生成コードを記述するのは、通常のコーディングとは大きく異なる作業です。コード生成コードの記述では解析ツリーをプログラム的に生成しますが、そのためには、通常の開発とは異なる考え方が必要です。
ここで言いたいのは、CodeDOM
は単純なコードを複数の言語で生成するときに適しているということです。本稿のPropertyUnionジェネレータのサンプルのように、型付きのDataSetジェネレータを作成するときには、CodeDOM
が適しています。しかし、複雑なアルゴリズムを生成しなければならない場合や、コードを複数の言語で生成する必要がない場合は、XSLTか商用のコードジェネレータを使用した方がよいでしょう。
以降では、コードを1行1行説明していくようなことはしません。最初はそうしようと思っていたのですが、長ったらしくて退屈な原稿になるのでやめました。CodeDOM
のコードは、型、変数、メソッド、アルゴリズムといったプログラミングの基本概念を非常に面倒な方法で表現するものなので、それを逐一追ったのでは退屈になるだけです。コードの詳細については、サンプルアプリケーションを見てください。以降では、コードの詳細を追う代わりに、CodeDOM
アプリケーションの動作を上位レベルの視点から見ていきます。
ステップ1:準備
CodeDOM
を使用するためには型操作を何度も行う必要があるので、今回のサンプルでは、すべての型操作を担当するInheritanceTree
という便利なクラスを用意しました(リスト1を参照)。基本的には、これはType
オブジェクトの型セーフなコレクションです。このクラスでは、ユーザーが同じ階層内にない複数の型(たとえばSystem.String
とSqlDataReader
)をPropertyUnion
クラスに入れるのを防ぐために、いくつかのチェックを行います。
public class InheritanceTree { /// <summary> /// Tree root. /// </summary> private Type root; /// <summary> /// Gets/Sets the tree root. /// </summary> /// <remarks> /// Throws an exception on set if the specified value isn't a member of the tree. /// </remarks> public Type Root { get{return root;} set { foreach(Type leaf in leaves) { if( !leaf.IsSubclassOf(value) ) { throw new ArgumentException("Root must be a supertype of all leaves."); } } root = value; } } private TypeCollection leaves; /// <summary> /// Gets a collection of inheritance tree leaves. /// </summary> public TypeCollection Leaves { get{return this.leaves;} } /// <summary> /// Enumerates property types-- getter and setter. /// </summary> public enum PropertyType { Reader, Writer, } /// <summary> /// Gets a TypeCollection holding all types that implement a specified property. /// </summary> /// <param name="propertyName"></param> /// <param name="propertyType"></param> /// <returns></returns> public TypeCollection GetPropertyImplementers ( string propertyName, PropertyType propertyType ) { TypeCollection implementingTypes = new TypeCollection(); foreach ( Type type in this.GetAllTypes() ) { PropertyInfo prop = type.GetProperty(propertyName); if( prop!=null && ((propertyType==PropertyType.Reader && prop.CanRead) || (propertyType==PropertyType.Writer && prop.CanWrite)) ) { implementingTypes.Add(type); } } return implementingTypes; } /// <summary> /// /// </summary> /// <returns></returns> public PropertyInfo[] GetPropertyUnion() { ArrayList propertyInfos = new ArrayList(); StringCollection propertyNames = new StringCollection(); foreach( Type type in GetAllTypes() ) { foreach( PropertyInfo property in type.GetProperties() ) { //TODO: Worry about getters and setters. if( !propertyNames.Contains(property.Name) ) { propertyInfos.Add(property); propertyNames.Add(property.Name); } } } PropertyInfo[] props = new PropertyInfo[propertyInfos.Count]; propertyInfos.CopyTo(props); return props; } public TypeCollection GetAllTypes() { TypeCollection allTypes = new TypeCollection(); allTypes.Add(this.root); foreach(Type type in this.leaves) { allTypes.Add(type); } return allTypes; } public InheritanceTree() { leaves = new TypeCollection(); root = typeof(object); leaves.BeforeInsert+=new TypeCollection.InsertHandler(OnBeforeInsert); leaves.BeforeSet+=new TypeCollection.SetHandler(OnBeforeSet); return; } private void OnBeforeInsert(int index, Type value) { if( !value.IsSubclassOf(root) ) { throw new ArgumentException("Specified leaf is not a subtype of the current tree."); } return; } private void OnBeforeSet(int index, Type oldValue, Type newValue) { if( !newValue.IsSubclassOf(root) ) { throw new ArgumentException("Specified leaf is not a subtype of the current tree."); } return; } }
InheritanceTree
クラスは、すべての型操作を担当するクラスです。このクラスは、Type
オブジェクトの型セーフなコレクションを作成し、いくつかのチェックを行って、コレクションに含まれる型がすべて同じ階層に属することを確認します。
サンプルコードには、Build
というユーティリティクラスも含まれています(最初からすぐにBuild
クラスを作成したわけではなく、あったら便利だなと思ったので後から作成しました)。このクラスの目的は、CodeDOM
のよくある操作(たとえ1行の操作でも)をラップして、コードの読みやすさを向上させることです。
ここでは、CodePropertySetValueReferenceExpression
クラスを例に取って説明します。CodeDOM
の手法では、言語の各種機能をクラスとして表現します。CodePropertySetValueReferenceExpression
クラスは、プロパティセッターで使用されるC#のvalue
キーワードと同じ役割を果たします。value
キーワードはC#固有の機能なので、CodeDOM
では使用できません。したがって、CodeDOM
では代わりにCodePropertySetValueReferenceExpression
クラスを使用します。
ここで紹介するような「糖衣構文(syntactic sugar)」を使用するかどうかは、個人の好みです。しかし私に言わせれば、次のコード行の方が、
new CodeAssignStatement(propRef, Build.Value);
次のコード行より読みやすいと思います。
new CodeAssignStatement(propRef, new CodePropertySetValueReferenceExpression() );
話をわかりやすくするために、ここで前述のCodeDOM
ステートメントをC#で表現するとどうなるかを紹介しておきます。
someExpression = value;
ステップ2:型の列挙
本稿の最初の方で、どの型をインスタンス化するかをデータ層に教えるために型識別子が必要であるという話をしたのを思い出してください。幸い、列挙型を作成することは最も簡単な(しかし重要な)CodeDOM
操作です。列挙はデータメンバを1つだけ持つクラスにすぎません。CodeDOM
名前空間を使っていると、「構造の方が内容よりも簡単である」ということに気付くでしょう。そして、列挙はすべて構造です。
次のコード例には、列挙を作成するために必要なコードがすべて含まれています。ここでは、すべての型をループ処理し、個々の型に関するメンバを追加しているだけです。
TypeCollection allTypes = inheritanceTree.GetAllTypes(); _typeEnum = new CodeTypeDeclaration( "TypeEnum" ); _typeEnum.IsEnum=true; for(int i = 0; i<allTypes.Count; i++) { Type t = allTypes[i]; CodeMemberField field = new CodeMemberField(); field.Name=t.Name; field.InitExpression= new CodePrimitiveExpression(i); _typeEnum.Members.Add(field); }
ステップ3:コンストラクタの作成
次に、コンストラクタを出力するコードを記述しなければなりません。既定のコンストラクタは簡単に作成できます。
private CodeConstructor BuildDefaultCtor() { CodeConstructor defaultConstructor = new CodeConstructor(); defaultConstructor.Attributes = MemberAttributes.Private; return defaultConstructor; }
この出力結果は次のとおりです。
private ContactPropertyUnion()
{
}
重要なコンストラクタの場合は、もう少し手間がかかります。次に示す例では、ラップ可能なオブジェクトへの参照を受け取り、それをデータメンバに割り当てています。先ほど、CodeDOM
では構造の方が内容より簡単であると述べたことを思い出してください。今度は内容について見ていきます。ここで出力しようとしているメソッドはわずか1行だけのものですが、CodeDOM
を使ってこのメソッドを記述するには10行以上のコードが必要です。
private CodeConstructor BuildWrapperCtor() { CodeConstructor wrapCtor = new CodeConstructor(); wrapCtor.Attributes=defaultVisibility; //Setup the single parameter. CodeParameterDeclarationExpression toWrapParam = new CodeParameterDeclarationExpression(); toWrapParam.Name="toWrap"; toWrapParam.Type = new CodeTypeReference( inheritanceTree.Root ); wrapCtor.Parameters.Add(toWrapParam); //Assign the parameter to the data member. CodeAssignStatement assign = new CodeAssignStatement(); assign.Left = WrappedFieldReference; assign.Right = new CodeVariableReferenceExpression( toWrapParam.Name); wrapCtor.Statements.Add(assign); return wrapCtor; }
出力結果は次のとおりです。
internal ContactPropertyUnion(Contact toWrap)
{
wrapped=toWrap;
}
ステップ4:プロパティ
最後に、PropertyUnion
クラスを自動生成するための核心部分について説明します。基本的に、各プロパティのゲッターは次の4段階の処理を行います。
- 既定の変数を初期化する(ラップされるオブジェクトが目的のプロパティを実装していない場合に備えて)
- ラップされるオブジェクトの型を調べる
- その型に応じて、ラップされるオブジェクトを具象型にキャストし、キャストオブジェクトの適切なプロパティを呼び出す
- 結果の値を返す
この方法はそれほど悪くありません。この4つの手順では、条件、プロパティの呼び出し、メソッドの呼び出しなど、主に内容を扱っています。リスト2では、System.Reflection
名前空間のPropertyInfo
オブジェクトに基づいてアクセッサを作成します。このコードリストは長めですが、少なくとも1回記述すれば済みます。
このメソッドはPropertyUnionBuilder
クラスの中で最も複雑な部分であり、割り当てと型判定処理を担当します。
/// <summary> /// Builds an property to access the /// specified property of the wrapped object. /// If the wrapped object doesn't actually implement /// the specified property, then just return /// a default value. /// </summary> /// <param name="toWrap"></param> /// <returns></returns> private CodeMemberProperty BuildAccessor(PropertyInfo toWrap) { CodeMemberProperty prop = new CodeMemberProperty(); prop.Name=toWrap.Name; prop.HasGet=toWrap.CanRead; prop.HasSet=toWrap.CanWrite; prop.Type= new CodeTypeReference( toWrap.PropertyType ); prop.Attributes = this.DefaultVisibility; if(toWrap.CanRead) { CodeVariableDeclarationStatement outValDeclaration = new CodeVariableDeclarationStatement( prop.Type, "outVal"); outValDeclaration.InitExpression = Build.Default( toWrap.PropertyType); prop.GetStatements.Add( outValDeclaration ); TypeCollection implementers = this.InheritanceTree.GetPropertyImplementers( toWrap.Name, InheritanceTree.PropertyType.Reader); // Since CodeDOM can't represent a switch statement, // we'll have to get by with nested "if"s. CodeStatementCollection bigBucket = new CodeStatementCollection(); CodeStatementCollection currentBucket = bigBucket; foreach( Type implementer in implementers ) { CodeConditionStatement ifStatement = new CodeConditionStatement(); currentBucket.Add(ifStatement); ifStatement.Condition = Build.Is( implementer, WrappedField.Name); CodeCastExpression caster = Build.Cast( Build.TypeRef(implementer), WrappedFieldReference ); CodePropertyReferenceExpression propRef = new CodePropertyReferenceExpression(); propRef.TargetObject= caster; propRef.PropertyName=toWrap.Name; CodeAssignStatement assignPropRefToOutVal = new CodeAssignStatement(); assignPropRefToOutVal.Left = Build.VarRef(outValDeclaration); assignPropRefToOutVal.Right = propRef; ifStatement.TrueStatements.Add(assignPropRefToOutVal); currentBucket = ifStatement.FalseStatements; } prop.GetStatements.AddRange(bigBucket); CodeMethodReturnStatement returnStatement = Build.Return; returnStatement.Expression= new CodeVariableReferenceExpression( outValDeclaration.Name); prop.GetStatements.Add(returnStatement); } if(toWrap.CanWrite) { TypeCollection implementers = InheritanceTree.GetPropertyImplementers(toWrap.Name, InheritanceTree.PropertyType.Reader); CodeStatementCollection bigBucket = new CodeStatementCollection(); CodeStatementCollection currentBucket = bigBucket; foreach( Type implementer in implementers ) { CodeConditionStatement ifStatement = new CodeConditionStatement(); currentBucket.Add(ifStatement); ifStatement.Condition = Build.Is(implementer, WrappedField.Name); CodeCastExpression caster = Build.Cast( Build.TypeRef(implementer), WrappedFieldReference ); CodePropertyReferenceExpression propRef = new CodePropertyReferenceExpression(); propRef.TargetObject= caster; propRef.PropertyName=toWrap.Name; CodeAssignStatement assignPropRefToOutVal = new CodeAssignStatement(); assignPropRefToOutVal.Left = propRef; assignPropRefToOutVal.Right = Build.Value; ifStatement.TrueStatements.Add(assignPropRefToOutVal); currentBucket = ifStatement.FalseStatements; } prop.SetStatements.AddRange(bigBucket); prop.SetStatements.Add(Build.Return); } return prop; }
以上が、CodeDOM
の手法を上位レベルの視点から見た様子です。コメントや既定値の作成など、ここで触れなかった詳細部分もあるので、それらについてはサンプルコードを参照してください。しかし、この手法の主なしくみはここで紹介したとおりです。他の場面でも、コード生成を扱うときにはコードできるだけ単純にしておくと役立ちます。
このPropertyUnionパターンがSingletonパターン以来の最も優れたデザインパターンであると言うつもりはありません。実際、それほどすごいものではありません。ただ、これは不恰好な型判定コードを1つのクラスにまとめるためには役立つパターンです。特に、アプリケーションのさまざまな層で型判定をする場合にはこれが役立ちます。
ここで重要なのは、パターンを最も単純な要素に簡略化し、これ以上できないところまで単純化すれば、コード生成を利用して再使用可能な実装を作成できる可能性があるということです。