働くプログラマ

マルチパラダイムと .NET (第 6 部): 反映メタプログラミング

Ted Neward

先月の続きです。先月は、自動メタプログラミングについて説明しました。ここまでの説明で、共通性と可変性の概念についてはおなじみになったでしょう。今月は、プログラムを作成するプログラムという、メタプログラミングの考え方について詳しく説明します。

先月は、よく目にするメタプログラミング手法の 1 つとして、自動メタプログラミングを紹介しました。自動メタプログラミングは、コード生成としてもよく知られています。自動メタプログラミングのシナリオでは、開発者は生成する "対象物" を表現するプログラムを作成します。通常、この作業は、コマンドラインのパラメーターや、リレーショナル データベース スキーマ、XSD ファイル、Web サービス記述言語 (WSDL) ドキュメントなどからの入力の支援を受けて行われます。

コード生成は基本的に、コードが人間の手によって記述された "かのように" 生成されるため、コード内であればどこにでも可変性を配置できます。データ型、メソッド、継承など、可変性の対象は必要に応じて変化します。もちろん、短所も 2 倍になります。まず、可変性が多くなりすぎると、生成されるコード (場合によっては、コードを生成するテンプレート) を理解するのが困難になります。次に、部分クラスを使用してコード生成をある程度分割している場合や、コード生成が必要なくなる場合を除いて、生成されたコードは基本的に編集できません。

さいわい、C# および Visual Basic には、Microsoft .NET Framework の初期段階から、単なる自動メタプログラミングよりも優れたメタプログラミング手法が用意されています。

永続化の課題

オブジェクト指向環境で再帰的に発生する課題は、オブジェクトからリレーショナルに永続化することに関する課題です。これは、オブジェクトから XML への変換の課題や、最近の Web 2.0 分野ではオブジェクトから JSON への変換の課題としても知られています。開発者が最善の努力を尽くしているにもかかわらず、オブジェクト モデルではなんらかの方法で CLR を使用しないで、ネットワーク間を移動するか、ディスクとの間でやり取りすることは避けられないように思えます。ただし、ここに問題があります。以前の設計モード (手続き型やオブジェクト指向) では、このジレンマに対する解決策が提供されません。

人物をコードでモデル化する、標準的かつ簡単な表現を考えてみましょう。

class Person {
  public string FirstName { get; set; }
  public string LastName { get; set; }
  public int Age { get; set; }

  public Person(string fn, string ln, int a) {
    FirstName = fn; LastName = ln; Age = a;
  }
}

おわかりのように、特に、(最も単純な形式の) プロパティとリレーショナル テーブルの列とが 1 対 1 に対応しており、このプロパティにはパブリック アクセスが可能なため、このオブジェクトのインスタンスを永続化することは難しくはありません。Person クラスのインスタンスを受け取り、少量のデータを抽出して SQL ステートメントに挿入し、結果のステートメントをデータベースに送信するプロシージャを作成できます。

class DB {
  public static bool Insert(Person p) {
    // Obtain connection (not shown)
    // Construct SQL
    string SQL = "INSERT INTO person VALUES (" +
      "'" + p.FirstName + "', " +
      "'" + p.LastName + "', " +
      p.Age + ")";
    // Send resulting SQL to database (not shown)
    // Return success or fail
    return true;
  }
}

この手続き型手法では、実に簡単にその短所が現れます。新しい型 (Pet、Instructor、Student など) を挿入することを考えると、ほとんど同じコードであっても新しいメソッドが必要になります。さらに悪いことには、パブリック API を公開しているプロパティと、列または内部フィールドとが 1 対 1 で対応していなければ、事態は急速に複雑になります。SQL ルーチンを作成している開発者が、永続化するフィールドと永続化しないフィールドを把握する必要があり、これはカプセル化に明らかに違反します。

設計の観点から見ると、オブジェクトからリレーショナルへの永続化の課題では、データを永続化する SQL 部分を共通性に取り込むことを考えます。そのため、データベース接続とトランザクションの管理を 1 か所で処理しますが、可変性は依然として永続化 (または取得) するデータの実際の構造に配置できます。

以前のコラムで述べたように、(正の) 可変性を許容している場合、手続き型手法はアルゴリズム上の共通性を捕捉し、継承は構造上の共通性を捕捉します。しかし、いずれも、必要なことが正確に実行されるわけではありません。共通性を基本クラスに配置する継承手法では、派生クラスで SQL 文字列を指定し、大量の入出力ブックキーピングを処理する必要があります (事実上、共通性が派生クラスに持ち込まれることになります)。手続き型手法では、(実行する SQL を抽出および構築する) プロシージャ内部になんらかの可変性が必要になり、この可変性はプロシージャの外部から指定するため、これを実現するのも比較的困難です。

メタプログラミングについて

頻繁に取り上げられるオブジェクトからリレーショナルへの永続化に関する課題の解決策の 1 つが自動メタプログラミングです。自動メタプログラミングでは、データベース スキーマを使用して、データベースとの間で自身を永続化する方法を理解するクラスを作成します。

残念なことに、この解決策には従来からのコード生成の問題がすべて含まれています。特に、クラスからオブジェクト表現に変更を加えて、物理データベース スキーマが使用する表現よりも使いやすい表現に変更したいと考えると問題が生じます。たとえば、char[2000] ではなく、.NET Framework の System.String の VARCHAR(2000) にすれば、その列は非常に使いやすくなると考えるような場合です。

他にも、まず、クラス定義を作成し、永続化クラスの定義に従ってデータベース スキーマを作成するコード生成手法があります。しかし、この手法では、オブジェクト階層が、永続化のモデルだけでなく、その他のことを行うモデルの 2 つに重複することになります (オブジェクトを XML に変換する必要が生じると、別の階層を用意して対応することになり、JSON にも対応することになればさらに別の階層を用意することになります。この手法はすぐに手に負えなくなります)。

さいわい、"反映メタプログラミング" にこの課題を解決する可能性があります。.NET Framework 1.0 から含まれている System.Reflection を使用して、開発者は実行時にオブジェクトの構造を調べることができます。今回の課題を解決する場合は、この永続化を意識したインフラストラクチャにより、永続化するオブジェクトの構造を調べ、その構造から必要な SQL を生成する機会が与えられます。System.Reflection の基本概要については、MSDN ドキュメント (msdn.microsoft.com/library/f7ykdhsy(v=VS.400)、英語)、および MSDN Magazine の記事「リフレクションを使用して .NET Framework でよく使用される型を検出して評価する」(msdn.microsoft.com/magazine/cc188926、英語) と「CLR 徹底解剖: リフレクションについての考察」(msdn.microsoft.com/magazine/cc163408) を参照してください。今回は、System.Reflection についてこれ以上触れません。

リフレクションは、カプセル化の外観をすべて保持し続けたまま、アルゴリズムの共通性を許容し、そのアルゴリズムが操作する構造の可変性も許容します。セキュリティ コンテキストを適切に構成していれば、リフレクションによってオブジェクトのプライベート メンバーにアクセスできるため、これらのデータ メンバーを公開する必要なく、内部データを操作できます。要素を追加することで変更できる正の可変性は、常に、簡単に操作でき、フィールドの数がリフレクションベースのコードに影響を与えることはほとんどありません。しかし、要素を削除することで変更できる負の可変性は、完全に適合するとは思えません。結局のところ、フィールドが存在しないクラスを永続化する必要はありません。さらに、ばかげた話のように思えるかもしれませんが、プライベート フィールドをループ処理するリフレクションベースのインフラストラクチャは、まったくループしなくなるためほとんど問題がなくなります。

ただし、ここで問題になる負の可変性は、単にフィールドを取り除く負の可変性とは少し異なります。シナリオによっては、Person クラスに、まったく永続化しなくてもよい内部フィールドが含まれることがあります。言い方を変えると、Person クラスには、CLR がホストする表現ではなく、異なるデータ形式での永続化を希望するフィールドが含まれることがあります。たとえば、Person.Birthdate は、文字列で格納する場合も、1 つの列ではなく、3 つの列 (日、月、年) に分けて格納する場合もあります。つまり、反映メタプログラミングにおける負の可変性とは、フィールドが存在しないという意味ではなく、標準の方法で処理する予定の型の特定のインスタンスに、異なる処理を実行するという意味になります (たとえば、文字列の標準では VARCHAR 列に永続化しますが、1 つまたは複数の特定のフィールドの文字列を BLOB 列に永続化することもあります)。

.NET Framework では、カスタム属性を使用して、このような負の可変性に対応します。開発者は、独自の処理を希望するクラス内の要素に、属性を使用してタグを付けます (オブジェクトのシリアル化の場合に @NotSerialized を付けるなど)。ただし、属性はそれ自体では何の意味も持たないことに注意してください。属性は、その属性を検索するコードに対する単なるフラグです。また、属性自体が負の可変性を提供するものではありませんが、負の可変性が配置されたときに見つけやすくなります。

属性は、正の可変性の対応にも使用できます。たとえば、.NET Framework では属性を使用してトランザクション処理の対応を判断しており、メソッドに属性が存在しなければ、トランザクションとしてどのような関係性もないことを示すと想定します。

鏡よ、鏡よ、鏡さん

反映メタプログラミングは、属性を使用しなくても、まったく新しい種類の可変性を確立します。反映メタプログラミングでは、(コンパイラのシンボルではなく) "名前" を使用してプログラム内の要素を参照できます。つまり、コンパイラで従来可能であったタイミングよりもはるかに遅く (実行時に) 参照できます。たとえば、NUnit という単体テスト フレームワークの初期リリースでは、Java 用の類似ツールの JUnit と同様、リフレクションを使用して名前が "test" で始まるメソッドを検出していました。つまり、このようなメソッドは、テスト スイートの一部として実行されるテスト メソッドであると想定していました。

名前ベースの手法を使用する場合、開発者は、一般的に人の目に留まる要素 (物の名前など) を採用する必要があります。また、NUnit のメソッドの "test" というプレフィックスのように、厳密な表記法に従う必要があります。カスタム属性を使用すると、名前ベースのような表記規則は緩和されます (このような表記規則は、問題のクラスで追加コードを構成することが必要になります) が、開発者は、基本的には、メタプログラムのメリットを活すための選択メカニズムを作成する必要があります。

また、属性には、属性のほかに、任意のデータにタグを付ける機能があります。この機能により、メタプログラムの動作に対して、非常に細かいパラメーター化を実現できます。これは通常、自動メタプログラミングでは使用できません。特に、クライアントが、構造が似たコンストラクターに異なる動作を指定することはできません (つまり、先ほどの例で言えば、文字列を VARCHAR 列ではなく BLOB 列に永続化させることはできません)。

ただし、リフレクションは、実行時にバインドされる性質により、リフレクションを広範囲に使用すると、コードのパフォーマンスに影響を与えることがよくあります。また、リフレクションによって、先月の自動メタプログラミングのシナリオで言及した課題 (クラスの急増など) に解決策が提供されることもありません。メタプログラミングの解決策はもう 1 つありますが、これについては来月までお待ちいただかなければなりません。

コーディングを楽しんでください。

Ted Neward は、Microsoft .NET Framework および Java のエンタープライズ プラットフォーム システムを専門とする独立企業 Neward & Associates の社長を務めています。これまでに 100 個を超える記事を執筆している Ted は、C# MVP であり、INETA の講演者でもあります。さまざまな書籍を執筆および共同執筆していて、『Professional F# 2.0』(Wrox、2010 年、英語) もその 1 つです。彼は定期的にコンサルティングを行い、開発者を指導しています。質問やコンサルティングの依頼については、ted@tedneward.com (英語のみ) に電子メールを送信してください。ブログを blogs.tedneward.com (英語) に公開しています。

この記事のレビューに協力してくれた技術スタッフの Anthony Green に心より感謝いたします。