|
ニュース検索
ピックアップ
今週のIT求人情報
|
CodeDOMコード生成を使用して反復パターンを実装するはじめに デザインパターンの専門家たちは、よく「パターンはコードではない」と言います。パターンはコードよりも上のレベルにあるのです。あるいは、Martin Fowlerが述べているように(PDF)、「パターンは設計上のアドバイスを正書法で表現するためのメカニズムである」と言えます。ここからわかるのは、パターンとは、オブジェクト指向のテクニックを使用して汎用的な形で実装するには抽象的すぎるということです。しかし、いくつかの下位レベルのパターン(「イディオム」とも呼ばれます)は、オブジェクト指向のテクニックではなくコード生成のテクニックを使って、再使用可能な形で実装することができます。本稿では、このようなパターンについて紹介するとともに、.NETの パート1:PropertyUnionパターンの概要PropertyUnion(プロパティの結合)パターンは非常に単純です。このパターンは、継承階層のすべての関連プロパティをフラットにして公開するラッパークラスです。継承階層をフラットにすることで、クライアントコードがオブジェクトのプロパティにアクセスする前に型チェックやキャストを行わずに済むようにします。 この説明では全然わからないという人は、このまま続きを読んでください。このパターンや類似パターンを既に使ったことがある人は、パート2に進んで、コード生成の説明から読んでもかまいません。 動機オブジェクト指向のプログラミング言語やデータベースでは、1つの問題ドメインを多様なアプローチでモデル化することができます。この違いを端的に指すために「インピーダンスの不一致」という用語が使われていますが、この不一致が最もよく現れるのは、継承階層をデータベーステーブルにマッピングするときです。従来のリレーショナルデータベースは継承をサポートしていないので、設計者は継承階層をフラットなリレーショナルモデルにマッピングするための方法を考え出さなければなりません。 具体的な例を紹介しましょう。たとえば単純な連絡先管理アプリケーションを作成するとします。従来の単純な連絡先と、従業員固有の追加データを含んだ従業員用の連絡先情報の両方を管理したいと考えています。これを実装する1つの方法は、具象基本クラスである 図1:単純なドメインモデル。EmployeesクラスにはContactクラス+αの情報を格納する必要があるので、このドメインは2つの関連クラスから構成されている ![]() 今度はデータモデルについて考えてみます。最も単純な方法は、「シングルテーブル継承」を使うことです。この手法では、階層全体を1つのテーブルで表し、そのテーブルの中に、階層内の全フィールドを結合させたフィールド群と、各行がどの型を表しているかを示す文字列型(または列挙体に関連付けられた整数型)の「型識別子」フィールドを用意します(図2)。 図2:単純なデータモデル。このデータベースではテーブルを1つだけ作成するが、階層をこのような方法でフラット化しているため、すべてのレコードにすべてのフィールドが適用されるとは限らない ![]() 表1は、サンプルデータに含まれる代表的な行セットを示しています。網掛けの部分は、 表1:このテーブルは、ContactsクラスとEmployeesクラスのプロパティの結合を示している。網掛けのない列はすべての連絡先に適用されるが、網掛け付きの列は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; } 上記のような重複したロジックを使用するのは混乱の素です。実際、 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; } このコードでは、 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パターンを使用する場合には型判定コードを記述せざるを得ませんが、それを中央にまとめておけば、少しは状況が改善されます。今回のサンプルアプリケーションでは、型判定コードを 図3:改良後のオブジェクトモデル。ContactPropertyUnionクラスの各インスタンスには、Contact型のインスタンスが含まれている ![]() このパターンでは、型識別子に基づいて 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."); }
たとえば次のコードは、このラッパークラスが 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; } } この 最後に、UI関係のコードの利便性を高めるために、 internal bool HasEmployeeNumberGetter { get { return wrapped is Employee; } } 従来のポリモーフィズムがユビキタス的であるのに比べて、PropertyUnionパターンはニッチ市場的と言えます。このパターンは次のような場面で使用します。
また、次のような場面ではPropertyUnionパターンを使用するべきではありません。
PropertyUnionパターンには、次のような関連パターンがあります。
このように、PropertyUnionパターンのテクニックは非常に簡単です。このパターンで使用するのは決まりきったコードだけであり、アルゴリズムを考えたり、何か特別なことをしたりする必要はありません。そのため、PropertyUnionパターンの実装を自動化する方法はないものかと考えた読者もいるのではないでしょうか。パート2ではそれについて見ていくことにします。 パート2:PropertyUnionの実装の自動化 単純な
そこで今度は、.NETの ただし、最初に注意しておきたいことがあります。
ここで言いたいのは、 以降では、コードを1行1行説明していくようなことはしません。最初はそうしようと思っていたのですが、長ったらしくて退屈な原稿になるのでやめました。 ステップ1:準備 リスト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; } } サンプルコードには、 ここでは、 ここで紹介するような「糖衣構文(syntactic sugar)」を使用するかどうかは、個人の好みです。しかし私に言わせれば、次のコード行の方が、
new CodeAssignStatement(propRef, Build.Value);
次のコード行より読みやすいと思います。 new CodeAssignStatement(propRef, new CodePropertySetValueReferenceExpression() ); 話をわかりやすくするために、ここで前述の someExpression = value; ステップ2:型の列挙 本稿の最初の方で、どの型をインスタンス化するかをデータ層に教えるために型識別子が必要であるという話をしたのを思い出してください。幸い、列挙型を作成することは最も簡単な(しかし重要な) 次のコード例には、列挙を作成するために必要なコードがすべて含まれています。ここでは、すべての型をループ処理し、個々の型に関するメンバを追加しているだけです。 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()
{
}
重要なコンストラクタの場合は、もう少し手間がかかります。次に示す例では、ラップ可能なオブジェクトへの参照を受け取り、それをデータメンバに割り当てています。先ほど、 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:プロパティ 最後に、
この方法はそれほど悪くありません。この4つの手順では、条件、プロパティの呼び出し、メソッドの呼び出しなど、主に内容を扱っています。リスト2では、 このメソッドは リスト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; } 以上が、 このPropertyUnionパターンがSingletonパターン以来の最も優れたデザインパターンであると言うつもりはありません。実際、それほどすごいものではありません。ただ、これは不恰好な型判定コードを1つのクラスにまとめるためには役立つパターンです。特に、アプリケーションのさまざまな層で型判定をする場合にはこれが役立ちます。 ここで重要なのは、パターンを最も単純な要素に簡略化し、これ以上できないところまで単純化すれば、コード生成を利用して再使用可能な実装を作成できる可能性があるということです。 著者紹介Eric McMullen(Eric McMullen)
デンバーを拠点とするコンサルティング企業Falstaff Solutionsのディレクター。同社はデータ中心の.NETアプリケーション開発を専門とする。Falstaffの詳細については同社Webサイト(www.falstaffsolutions.com)を参照。
新着ニュース・コラム ホワイトペーパー
|
注目のトピックス 話題の記事
企業の約4割がいまでも IE 6 以前のブラウザを利用 ― Web 広告研究会調査
SNS「非モテ+」、バレンタイン関連ワード投稿を禁止に
さぬきうどんの食べ歩きをサポートするスマホアプリ「Udooon!」が公開
Android 版 Chrome ベータ1登場、ただし Android 4.0に限る
Android アプリを美しくみせる UI デザイン10のヒント
⇒一覧を見る
アクセスランキング
最新コラム一覧
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||