SHOEISHA iD

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

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

japan.internet.com翻訳記事

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

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

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

単純なパターンを実装するのは、それが単純であるだけに、退屈で時間がかかり、間違いを起こしやすいものです。しかしコード生成を利用すれば、この退屈な仕事の大部分を省くことができます。

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

はじめに

 デザインパターンの専門家たちは、よく「パターンはコードではない」と言います。パターンはコードよりも上のレベルにあるのです。あるいは、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の実装の自動化

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

  • 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」など、さまざまなカンファレンスを企画・運営しています。

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

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

メールバックナンバー

アクセスランキング

アクセスランキング