はじめに
デザインパターンの専門家たちは、よく「パターンはコードではない」と言います。パターンはコードよりも上のレベルにあるのです。あるいは、Martin Fowlerが述べているように(PDF)、「パターンは設計上のアドバイスを正書法で表現するためのメカニズムである」と言えます。ここからわかるのは、パターンとは、オブジェクト指向のテクニックを使用して汎用的な形で実装するには抽象的すぎるということです。しかし、いくつかの下位レベルのパターン(「イディオム」とも呼ばれます)は、オブジェクト指向のテクニックではなくコード生成のテクニックを使って、再使用可能な形で実装することができます。本稿では、このようなパターンについて紹介するとともに、.NETのCodeDOM
名前空間を使って再使用可能な実装を作成する方法について解説します。
パート1:PropertyUnionパターンの概要
PropertyUnion(プロパティの結合)パターンは非常に単純です。このパターンは、継承階層のすべての関連プロパティをフラットにして公開するラッパークラスです。継承階層をフラットにすることで、クライアントコードがオブジェクトのプロパティにアクセスする前に型チェックやキャストを行わずに済むようにします。
この説明では全然わからないという人は、このまま続きを読んでください。このパターンや類似パターンを既に使ったことがある人は、パート2に進んで、コード生成の説明から読んでもかまいません。
動機
オブジェクト指向のプログラミング言語やデータベースでは、1つの問題ドメインを多様なアプローチでモデル化することができます。この違いを端的に指すために「インピーダンスの不一致」という用語が使われていますが、この不一致が最もよく現れるのは、継承階層をデータベーステーブルにマッピングするときです。従来のリレーショナルデータベースは継承をサポートしていないので、設計者は継承階層をフラットなリレーショナルモデルにマッピングするための方法を考え出さなければなりません。
具体的な例を紹介しましょう。たとえば単純な連絡先管理アプリケーションを作成するとします。従来の単純な連絡先と、従業員固有の追加データを含んだ従業員用の連絡先情報の両方を管理したいと考えています。これを実装する1つの方法は、具象基本クラスであるContact
とそのサブクラスEmployee
から成る継承階層を作成することです(図1)。
今度はデータモデルについて考えてみます。最も単純な方法は、「シングルテーブル継承」を使うことです。この手法では、階層全体を1つのテーブルで表し、そのテーブルの中に、階層内の全フィールドを結合させたフィールド群と、各行がどの型を表しているかを示す文字列型(または列挙体に関連付けられた整数型)の「型識別子」フィールドを用意します(図2)。
表1は、サンプルデータに含まれる代表的な行セットを示しています。網掛けの部分は、Employee
型の連絡先にのみ適用されるフィールドです。
ContactType | FamilyName | GivenName | BirthDate | Title | Salary | MailStop |
Contact | Bloggs | Joe | 1977.07.03 | |||
Contact | Pilgrim | Billy | 1924.02.12 | |||
Employee | Holden | Judge | 1822.01.01 | President | 10 | 12d |
Employee | Slothrup | Tyrone | 1922.03.12 | Peon | 3 | 44f |
この手法には、複数のテーブルを使用するデータモデルに比べて次のようなメリットがあります。
- 単純:テーブルが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
クラスにまとめます。
このパターンでは、型識別子に基づいて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ではそれについて見ていくことにします。