SHOEISHA iD

※旧SEメンバーシップ会員の方は、同じ登録情報(メールアドレス&パスワード)でログインいただけます

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

japan.internet.com翻訳記事

CodeDOMコード生成を使用して反復パターンを実装する

コード生成を利用した再使用可能な実装の作成

  • X ポスト
  • このエントリーをはてなブックマークに追加

パート2:PropertyUnionの実装の自動化

 単純なContactPropertyUnionクラスを手作業で記述してもかまいませんが、そうすると次のような問題が出てきます。

  • 継承階層を変更するときに、PropertyUnionクラスにも同等の変更を加える必要がある
  • 型判定をもっと複雑な継承階層と組み合わせる場合は、バグの原因になりやすい
  • そもそもPropertyUnionクラスを記述しなければならないことが手間である。これはごく単純な実装で、必要なのは、ラップする継承階層といくつかのメタデータをビルド時に型情報としてコンパイラに渡すことだけである

 そこで今度は、.NETのCodeDOM名前空間を使用してPropertyUnionBuiderを実装する方法を見ていきたいと思います。CodeDOMとは、言語に依存しないコード記述コードを書くためのライブラリです。

 ただし、最初に注意しておきたいことがあります。CodeDOMを使用すると、コードを記述するコードを作成できます。さらに、このコード記述コードでは任意の.NET言語のコードを記述できます。その点だけを見れば、最初は「素晴らしい!」と思うかもしれません。しかし、素晴らしいことばかりではありません。実際には、CodeDOM名前空間にはいくつか面倒な点があります。

  • CodeDOMは言語固有のイディオムをサポートしていない:CodeDOM名前空間はすべての.NET言語の最小公分母を表しているので、asisといった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.StringSqlDataReader)をPropertyUnionクラスに入れるのを防ぐために、いくつかのチェックを行います。

リスト1 InheritanceTreeクラス
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段階の処理を行います。

  1. 既定の変数を初期化する(ラップされるオブジェクトが目的のプロパティを実装していない場合に備えて)
  2. ラップされるオブジェクトの型を調べる
  3. その型に応じて、ラップされるオブジェクトを具象型にキャストし、キャストオブジェクトの適切なプロパティを呼び出す
  4. 結果の値を返す

 この方法はそれほど悪くありません。この4つの手順では、条件、プロパティの呼び出し、メソッドの呼び出しなど、主に内容を扱っています。リスト2では、System.Reflection名前空間のPropertyInfoオブジェクトに基づいてアクセッサを作成します。このコードリストは長めですが、少なくとも1回記述すれば済みます。

 このメソッドはPropertyUnionBuilderクラスの中で最も複雑な部分であり、割り当てと型判定処理を担当します。

リスト2 アクセッサの作成
/// <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つのクラスにまとめるためには役立つパターンです。特に、アプリケーションのさまざまな層で型判定をする場合にはこれが役立ちます。

 ここで重要なのは、パターンを最も単純な要素に簡略化し、これ以上できないところまで単純化すれば、コード生成を利用して再使用可能な実装を作成できる可能性があるということです。

この記事は参考になりましたか?

  • X ポスト
  • このエントリーをはてなブックマークに追加
japan.internet.com翻訳記事連載記事一覧

もっと読む

この記事の著者

japan.internet.com(ジャパンインターネットコム)

japan.internet.com は、1999年9月にオープンした、日本初のネットビジネス専門ニュースサイト。月間2億以上のページビューを誇る米国 Jupitermedia Corporation (Nasdaq: JUPM) のニュースサイト internet.comEarthWeb.com からの最新記事を日本語に翻訳して掲載するとともに、日本独自のネットビジネス関連記事やレポートを配信。

※プロフィールは、執筆時点、または直近の記事の寄稿時点での内容です

Eric McMullen(Eric McMullen)

デンバーを拠点とするコンサルティング企業Falstaff Solutionsのディレクター。同社はデータ中心の.NETアプリケーション開発を専門とする。Falstaffの詳細については同社Webサイト(www.falstaffsolutions.com)を参照。

※プロフィールは、執筆時点、または直近の記事の寄稿時点での内容です

この記事は参考になりましたか?

この記事をシェア

  • X ポスト
  • このエントリーをはてなブックマークに追加
CodeZine(コードジン)
https://codezine.jp/article/detail/91 2005/09/07 14:58

おすすめ

アクセスランキング

アクセスランキング

イベント

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

新規会員登録無料のご案内

  • ・全ての過去記事が閲覧できます
  • ・会員限定メルマガを受信できます

メールバックナンバー

アクセスランキング

アクセスランキング