japan.internet.comThe Internet & IT Network
RSS
  • ニュース
  • コラム
  • リサーチ
  • ヘッドライン
  • 特集
  • ブログ
  • プレスリリース
  • 専門チャンネル
  • イベント
  • ランキング
  • ニュースメール
2009年7月4日
文字サイズ文字サイズ小文字サイズ中文字サイズ大
デベロッパー2005年9月6日 11:00

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

海外海外internet.com発の記事
  • このエントリーを含むはてなブックマーク
  • この記事をクリップ!
  • Buzzurlにブックマーク
  • Yahoo!ブックマークに登録
  • newsing it!
  • この記事をokyuuへインポート

はじめに

 デザインパターンの専門家たちは、よく「パターンはコードではない」と言います。パターンはコードよりも上のレベルにあるのです。あるいは、Martin Fowlerが述べているように(PDF)、「パターンは設計上のアドバイスを正書法で表現するためのメカニズムである」と言えます。ここからわかるのは、パターンとは、オブジェクト指向のテクニックを使用して汎用的な形で実装するには抽象的すぎるということです。しかし、いくつかの下位レベルのパターン(「イディオム」とも呼ばれます)は、オブジェクト指向のテクニックではなくコード生成のテクニックを使って、再使用可能な形で実装することができます。本稿では、このようなパターンについて紹介するとともに、.NETのCodeDOM名前空間を使って再使用可能な実装を作成する方法について解説します。

パート1:PropertyUnionパターンの概要

 PropertyUnion(プロパティの結合)パターンは非常に単純です。このパターンは、継承階層のすべての関連プロパティをフラットにして公開するラッパークラスです。継承階層をフラットにすることで、クライアントコードがオブジェクトのプロパティにアクセスする前に型チェックやキャストを行わずに済むようにします。

 この説明では全然わからないという人は、このまま続きを読んでください。このパターンや類似パターンを既に使ったことがある人は、パート2に進んで、コード生成の説明から読んでもかまいません。

動機

 オブジェクト指向のプログラミング言語やデータベースでは、1つの問題ドメインを多様なアプローチでモデル化することができます。この違いを端的に指すために「インピーダンスの不一致」という用語が使われていますが、この不一致が最もよく現れるのは、継承階層をデータベーステーブルにマッピングするときです。従来のリレーショナルデータベースは継承をサポートしていないので、設計者は継承階層をフラットなリレーショナルモデルにマッピングするための方法を考え出さなければなりません。

 具体的な例を紹介しましょう。たとえば単純な連絡先管理アプリケーションを作成するとします。従来の単純な連絡先と、従業員固有の追加データを含んだ従業員用の連絡先情報の両方を管理したいと考えています。これを実装する1つの方法は、具象基本クラスであるContactとそのサブクラスEmployeeから成る継承階層を作成することです(図1)。

図1:単純なドメインモデル。EmployeesクラスにはContactクラス+αの情報を格納する必要があるので、このドメインは2つの関連クラスから構成されている
図1:単純なドメインモデル。EmployeesクラスにはContactクラス+αの情報を格納する必要があるので、このドメインは2つの関連クラスから構成されている

 今度はデータモデルについて考えてみます。最も単純な方法は、「シングルテーブル継承」を使うことです。この手法では、階層全体を1つのテーブルで表し、そのテーブルの中に、階層内の全フィールドを結合させたフィールド群と、各行がどの型を表しているかを示す文字列型(または列挙体に関連付けられた整数型)の「型識別子」フィールドを用意します(図2)。

図2:単純なデータモデル。このデータベースではテーブルを1つだけ作成するが、階層をこのような方法でフラット化しているため、すべてのレコードにすべてのフィールドが適用されるとは限らない
図2:単純なデータモデル。このデータベースではテーブルを1つだけ作成するが、階層をこのような方法でフラット化しているため、すべてのレコードにすべてのフィールドが適用されるとは限らない

 表1は、サンプルデータに含まれる代表的な行セットを示しています。網掛けの部分は、Employee型の連絡先にのみ適用されるフィールドです。

表1:このテーブルは、ContactsクラスとEmployeesクラスのプロパティの結合を示している。網掛けのない列はすべての連絡先に適用されるが、網掛け付きの列はEmployee型の連絡先にのみ適用される
ContactTypeFamilyNameGivenNameBirthDateTitleSalaryMailStop
ContactBloggsJoe1977.07.03
ContactPilgrimBilly1924.02.12
EmployeeHoldenJudge1822.01.01President1012d
EmployeeSlothrupTyrone1922.03.12Peon344f

 この手法には、複数のテーブルを使用するデータモデルに比べて次のようなメリットがあります。

  • 単純:テーブルが1つしかないのでレポートを簡単に記述できます。
  • パフォーマンス:テーブルが1つしかないので、どの型の従業員を扱う場合でも、データ層がデータベースに1回だけアクセスすれば済みます。
  • 保守性:テーブルが1つしかないので、データモデルに影響を与えずに、プロパティをオブジェクトモデルのさまざまなレベルにリファクタリングできます。

 次は実際にデータ層を記述してみます。データベース内の行に基づいてオブジェクトを作成する最もわかりやすい方法は、型識別子で切り替えることです。次のデータ層メソッドでは、明示的に型指定されたデータ行に基づいて、ContactオブジェクトまたはEmployeeオブジェクトを作成します。

private static Contact BuildContact(
   Contacts.ContactRow contactRow)
{
   Contact contact = null;         
   switch(contactRow.ContactType)
   {
      case 0:
         contact = new Contact();
         break;
      case 1:
         Employee asEmployee = new Employee();
         asEmployee.Salary = contactRow.Salary;
         asEmployee.EmployeeNumber = 
            contactRow.EmployeeNumber;
         asEmployee.MailStop = contactRow.MailStop;
         asEmployee.Title = (Employee.JobTitle) 
            contactRow.Title ;
         contact = asEmployee;
         break;
      default:
         throw new Exception("Unknown ContactType");
   }
   contact.BirthDate=contactRow.BirthDate;
   contact.FamilyName=contactRow.FamilyName;
   contact.GivenName=contactRow.GivenName;

   return contact;
}

 これはあまりうまい方法ではありません。また、非常に素朴でもあります。この点は、ユーザーインターフェイスを作成するときに大きな問題になります。次に示すUI層コードを見てもわかるとおり、各種UIウィジェットに値を割り当てるときに、編集する連絡先オブジェクトの型に応じてコードを分けなければなりません。

private void EditContact(Contact contact)
{
   Employee employee = contact as Employee;
   bool isEmployee = employee != null;

   this.tbEmployeeNo.Enabled = isEmployee;
   this.tbMailStop.Enabled = isEmployee;
   this.nudSalary.Enabled = isEmployee;
   this.cbTitle.Enabled = isEmployee;

   this.tbGivenName.Text=contact.GivenName;
   this.tbFamilyName.Text=contact.FamilyName;
   this.dtpBirthDate.Value = contact.BirthDate;

   if(isEmployee)
   {
      this.tbEmployeeNo.Text = 
         employee.EmployeeNumber.ToString();
      this.tbMailStop.Text = employee.MailStop;
      this.cbTitle.SelectedItem = employee.Title;
      this.nudSalary.Value=employee.Salary;
   }
   return;
}

 上記のような重複したロジックを使用するのは混乱の素です。実際、EmployeeオブジェクトとContactオブジェクトをデータ層とUI層の両方で作成・修正するので、最終的には同じコードを4回も記述することになります。

PropertyUnionパターンの実装

 それでは、重複したロジックをどのように処理すればよいでしょうか?答えは「カプセル化」です。これから紹介するPropertyUnionパターンでは、型判定コードを1つのクラスに移動して、継承階層をフラットにします。これにより、クライアントコードは、継承階層内のすべての型を統一化された方法で扱えるようになります。

 具体的な実装のしくみは後で説明するので、まずはクライアントコードを見てみましょう。次に示すデータ層メソッドは、先ほど紹介したものよりずっと簡単になっています。機能的にはまったく同じですが、型判定を実装していない(したがって重複がない)ことに注目してください。

private static Contact BuildContact(
   Contacts.ContactRow contactRow)
{
   ContactPropertyUnion.TypeEnum type = 
      (ContactPropertyUnion.TypeEnum)
      contactRow.ContactType;
   ContactPropertyUnion union = new 
      ContactPropertyUnion(type);

   union.Salary = contactRow.Salary;
   union.EmployeeNumber = contactRow.EmployeeNumber;
   union.MailStop = contactRow.MailStop;
   union.Title = (Employee.JobTitle) 
      contactRow.Title ;
   union.BirthDate=contactRow.BirthDate;
   union.FamilyName=contactRow.FamilyName;
   union.GivenName=contactRow.GivenName;

   return union.Wrapped;
}

 このコードでは、PropertyUnionクラスのインスタンスを使用してデータを設定しています。次に示すUI層メソッドでも、PropertyUnionクラスを使用してUIウィジェットを有効にし、各ウィジェットに適切なデータを割り当てています。このメソッドにも、型判定のコードは含まれていません。

private void EditContact(Contact contact)
{
   ContactPropertyUnion wrapper = new 
      ContactPropertyUnion(contact);

   this.tbEmployeeNo.Enabled = 
      wrapper.HasEmployeeNumberGetter;
   this.tbMailStop.Enabled = 
      wrapper.HasMailStopGetter;
   this.nudSalary.Enabled = wrapper.HasSalaryGetter;
   this.cbTitle.Enabled = wrapper.HasTitleGetter;

   this.tbGivenName.Text=wrapper.GivenName;
   this.tbFamilyName.Text=wrapper.FamilyName;
   this.dtpBirthDate.Value = wrapper.BirthDate;
   this.tbEmployeeNo.Text = 
      wrapper.EmployeeNumber.ToString();
   this.tbMailStop.Text = wrapper.MailStop;
   this.cbTitle.SelectedItem = wrapper.Title; 
   this.nudSalary.Value=wrapper.Salary;

   return;
}

 考え方としては、PropertyUnionパターンはオブジェクト指向のポリモーフィズムの基本概念によく似ています。どちらのテクニックでも、クライアントコードは関連オブジェクト内の情報量の違いを無視できます。ポリモーフィズムでは、使用可能なプロパティの交差(つまりすべての関連オブジェクトに共通するメンバセット)を公開します。一方、PropertyUnionパターンでは、継承階層内のすべてのプロパティの結合を公開します。

ContactPropertyUnionクラス

 オブジェクト指向設計には、暗黙的な基本原則がもう1つあります。それは、何か不恰好な処理をしなければならない場合(型判定は間違いなくその部類に入ります)は、その不恰好な部分をすべて1か所にまとめてしまうというものです。PropertyUnionパターンを使用する場合には型判定コードを記述せざるを得ませんが、それを中央にまとめておけば、少しは状況が改善されます。今回のサンプルアプリケーションでは、型判定コードをContactPropertyUnionクラスにまとめます。

図3:改良後のオブジェクトモデル。ContactPropertyUnionクラスの各インスタンスには、Contact型のインスタンスが含まれている
図3:改良後のオブジェクトモデル。ContactPropertyUnionクラスの各インスタンスには、Contact型のインスタンスが含まれている

 このパターンでは、型識別子に基づいてContactクラスまたはEmployeeクラスをインスタンス化するラッパークラスContactPropertyUnionを使用します。このクラスのコンストラクタを次に示します。このコンストラクタでは、数値ではなく列挙値を型識別子として使用します。

internal ContactPropertyUnion(TypeEnum toBuild)
{
   if (toBuild == TypeEnum.Contact)
   {
      this.wrapped = new Contact();
      return;
   }
   if (toBuild == TypeEnum.Employee)
   {
      this.wrapped = new Employee();
      return;
   }
   throw new ArgumentException("Unknown enum value.");
}

 ContactPropertyUnionクラスは、自身がラップしているすべての型のすべてのプロパティを公開します。ラップされているすべてのオブジェクトがすべてのプロパティを実際に実装しているとは限らないので、各プロパティでは独自に型判定を行います。ラップされているオブジェクトが目的のプロパティを実装していない場合は、次の2つのことが起こります。

  • ゲッターメソッドが既定値を返す
  • セッターメソッドが値を破棄する

 たとえば次のコードは、このラッパークラスがEmployeeNumberプロパティを公開するしくみを示しています。コードを見てもわかるとおり、基本となるクラスが実際にそのプロパティを実装しているかどうかは重要ではありません。使用する側は、常にそのプロパティがあるものとして扱うことができます。

internal int EmployeeNumber
{
   get
   {
      int outVal = new int();
      if ( wrapped is Employee )
      {
         outVal = (wrapped as 
            Employee).EmployeeNumber;
      }
      return outVal;
   }
   set
   {
      if (wrapped is Employee)
      {
         (wrapped as Employee).EmployeeNumber = value;
      }
      return;
   }
}

 このEmployeeNumberプロパティと同様のコードを、ラップされるクラスのすべてのプロパティ(または少なくともクライアントコードから利用される可能性のあるすべてのプロパティ)に対して繰り返します。

 最後に、UI関係のコードの利便性を高めるために、ContactPropertyUnionクラスにいくつかのブール型プロパティを実装します。これらのプロパティは、ラップされるクラスが該当プロパティを実装しているときにtrueを返します。

internal bool HasEmployeeNumberGetter
{
   get
   {
       return wrapped is Employee;
   }
}

 従来のポリモーフィズムがユビキタス的であるのに比べて、PropertyUnionパターンはニッチ市場的と言えます。このパターンは次のような場面で使用します。

  • データ層:単一テーブル継承データモデルを使用するときに、PropertyUnionをInheritance Mapperの軽量版として利用します。
  • UI層:Webフォーム上などでPropertyEditorコントロールを使用できない場合に、関連する型の数をアプリケーションのユーザーに編集させる必要があるときにPropertyUnionを使用します。
  • フレームワーク型を使用するとき:上記のオブジェクトモデルは、ある要件に対しては素朴すぎると評する開発者もいます。彼らの言うことにも一理ありますが、格納するオブジェクトの設計を常に制御できるとは限りません。

 また、次のような場面ではPropertyUnionパターンを使用するべきではありません。

  • 不適切な継承の尻ぬぐい:継承は気軽に乱用しやすい手法です。もっと洗練された適切なパターンがあるのに安易に継承を多用し、その尻ぬぐいとしてPropertyUnionパターンを利用することがないよう注意してください。

 PropertyUnionパターンには、次のような関連パターンがあります。

  • Factory:PropertyUnionパターンでは、ラップされるオブジェクトを型識別子に基づいて作成できるので、コンストラクタがファクトリメソッドのような働きをします。
  • Adapter:PropertyUnionパターンでは、継承階層をフラットにし、フラットなリレーショナル構造で扱いやすいようにします。

 このように、PropertyUnionパターンのテクニックは非常に簡単です。このパターンで使用するのは決まりきったコードだけであり、アルゴリズムを考えたり、何か特別なことをしたりする必要はありません。そのため、PropertyUnionパターンの実装を自動化する方法はないものかと考えた読者もいるのではないでしょうか。パート2ではそれについて見ていくことにします。

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

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

著者紹介

Eric McMullen(Eric McMullen)
デンバーを拠点とするコンサルティング企業Falstaff Solutionsのディレクター。同社はデータ中心の.NETアプリケーション開発を専門とする。Falstaffの詳細については同社Webサイト(www.falstaffsolutions.com)を参照。
このエントリーを含むはてなブックマーク この記事をクリップ!
BuzzurlにブックマークBuzzurlにブックマーク Yahoo!ブックマークに登録
この記事をokyuuへインポート
最新トップニュース
データメーション
【データメーション】
中国が「Green Dam」フィルタ規制を撤回(7月1日)
Graphic Design Forum
【Graphic Design Forum】
Chris Dickman(6月25日)
プライバシー ジャパン・インターネットコム版
【プライバシー ジャパン・インターネットコム版】
グーグル・ストリートビューの問題について総務省の見解(6月23日)
エンジニアの独り言
【エンジニアの独り言】
システムを「使う」時代のエンジニアに求められるもの(6月2日)
最新ハイテク講座
最新ハイテク講座
電気は家庭でつくる時代へ!燃料電池「エネファーム」(7月3日)
アクセス解析で見るWebマーケティング
アクセス解析で見るWebマーケティング
決定力を探るアクセス解析(7月3日)
百式のネットビジネス研究
百式のネットビジネス研究
ファーストフードを高級っぽく盛り付けて紹介している「Fancy Fast Food」(7月3日)
週刊-サイト別アクセス状況データ
週刊-サイト別アクセス状況データ
ビデオリサーチインタラクティブ調査(月間インターネットオーディエンスデータ)(7月2日)
成約率、反応率を上げる Web 文章術
成約率、反応率を上げる Web 文章術
言葉がダイレクトにキャッシュを生む(7月2日)
不況時代の Web ビジネス最適化講座
不況時代の Web ビジネス最適化講座
アクセス解析エキスパートここだけの話、Web コンシェルジュの“勉強法”こっそり教えます(7月2日)
「Webからの脅威」―その傾向と最新対策
「Webからの脅威」―その傾向と最新対策
不正プログラムの分類(7月1日)
DevX
DevX
JavaScriptとDOMによる動的なWebページの作成(6月30日)
エンジニア転職ノウハウ開発室
エンジニア転職ノウハウ開発室
今のままで大丈夫?3匹の子ブタ的キャリア危険度診断(6月30日)
アイレップの SEM フロンティア
アイレップの SEM フロンティア
Web サイトは「無駄な穴のたくさん開いたじょうご」〜サイト成果向上の基本的な考え方(6月30日)
Copyright 2009 Japan Internet.com K.K. All Rights Reserved.http://www.internet.com/