T4 テンプレート

T4 を使用してコード生成への懸念を取り除く

Peter Vogel

 

Microsoft .NET Framework では、デザイン時 (デザイン サーフェイスにコントロールをドラッグしてコードを生成するとき) にも、実行時 (LINQ によってデータを取得する SQL ステートメントを生成するとき) にもコード生成を多用します。コード生成を使用すれば、開発者が記述するコード量が減るため、明らかに生産性が向上します。特に、程度の差はあれ、ほぼ同一のコードが多くのソリューションで繰り返し使用される場合に役立ちます。新しいアプリケーションで、このようによく似た (ただし同一ではない) コードを実装するときは、残念ながら新たなエラーを生み出しやすくなります。

.NET Framework がアプリケーションの作成時に使用するすべてのコード生成ツールは開発者も利用することができますが、日常の開発作業にコード生成を頻繁に使用する開発者はほとんどいません。これにはさまざまな理由があります。開発者は、コード生成を組み込む際に新しいツールセットが必要になると、このツールセットの習得時間を取れないことを懸念します。コード生成で解決できる問題についての理解と、生成されたコードと "手書き" のコードを統合するソリューションを設計する経験が不足しています。そのため、コード生成ソリューションを利用しても、ソリューションを当てはめることで短縮される時間よりも、ソリューションのメンテナンス (または使用) にかかる時間の方が多いと感じています。

Microsoft Text Template Transformation Toolkit (T4) には、開発者が使い慣れたツールや手法を使用するコード生成ソリューションを簡単に実装できる方法が用意されているため、このような問題の多くに対処できます。T4 はコード生成を行うときに、典型的なソリューションには 2 種類のコードがあるという認識に基づきます。この 2 種類のコードとは、コード生成の状況が変わっても変化しない定型コードと、コード生成の状況が変わると変化する動的コードです。T4 は、開発者がソリューションの定型コード部分のみファイルに入力できるようにして、コード生成を簡略化します。T4 で動的コード (通常は、コード生成ソリューションのごく一部) を生成する場合は、ASP.NET MVC 開発者がビューの作成時に使用するタグによく似た一連のタグ、または ASP.NET 開発者が .aspx ファイルにサーバー側コードを埋め込むときに使用する一連のタグを使用します。

T4 ベースのソリューションを使用する場合、開発者は使用経験のあるプログラミング言語でコード生成の入力を指定できるため、習得済みのスキルを活用できます。T4 は、コードに依存しないソリューションは生成しません。T4 ソリューションが生成するコードは、常に、特定のプログラミング言語になります。コードに依存しないソリューションを必要とする開発者はめったにいません。

コード生成ソリューションを定義する

T4 を使用するとコード生成ソリューションの作成が簡単になりますが、コード生成ソリューションが役に立つ状況を認識する際や、実際のソリューションを設計する際に、開発者が抱える問題には対処しません。コード生成が解決する問題は、通常、次の 3 つの特性があります。

  1. 生成されるコードは、開発者が記述するコードとは別のファイルに収容します。これにより、コード生成プロセスが開発者のコードに影響しないようにします。生成されるコードは、通常、開発者が "手書き" のコードから使用する新しいクラスに含めます。開発者が呼び出すだけでなく拡張もできる部分クラスの生成が必要になる場合がありますが、それでも、開発者のコードと生成されるコードは別のファイルに収容されます。
  2. 生成されるコードは、反復的なコードです。ソリューションのコードは、何度でも繰り返し使用できるテンプレートで、一部変更して使用することもできます。そのため、コード生成は、同等の手書きのコードよりもメンテナンスが単純かつ簡単です。
  3. 手書きのソリューションと比較すると、生成されたソリューションに対して入力する必要のあるコードは大幅に少なくなります (何も入力しなくても、コード生成ソリューション自体が環境を調べて実行すべき処理を特定するのが理想です)。入力数が多い場合や、判断が困難な場合、開発者はコード生成ソリューションよりも手書きのソリューションの方がわかりやすいと考えます。

これらの特性を前提に、コード生成については 3 種類のシナリオを調べてみる価値があります。開発者は、多くの場合、1 つ目のシナリオ ("究極のシナリオ") に注目します。このシナリオでは、ごくわずかの入力で、頻繁に使用されるたくさんのコードが生成されます (この例として、Entity Framework があります)。しかし、実際のところ、コード生成を使用するうえで最も有益なのは、他の 2 つのシナリオです。

2 つ目のシナリオが最もわかりやすく、大量のコードを生成する必要があるときに使用します。この場合、複数行の反復コードを記述しなくてもよい点に明らかにメリットがあります。コード生成ソリューションのほとんどは複数のアプリケーションで使用されますが、このシナリオのソリューションは、1 つのアプリケーションで使用する場合にも真価を発揮します。コード生成ソリューションは、それぞれの状況に処理するために、If ステートメントを多く含む汎用化したコードを作成するのではなく (こうすると、各 If ステートメントによってコード ロジックの複雑さが倍増します)、条件のセットごとに必要な固有のコードを生成します。インターフェイスを共有する多くのクラスを手作業でコーディングするのではなく、コード生成を使用してクラスを個別に作成できます (クラスが共通の構造を共有していることが前提です)。

ただし、3 つ目のシナリオが最も一般的で、多くのアプリケーションから使用できる少量のコードを少量の入力で生成するときに使用します。このシナリオでは、特定のアプリケーション内で繰り返されるコードは少量ですが、コードがサポートする動作は共通のものなので、最終的に複数のアプリケーションに大量のコードを生成します。

たとえば、ADO.NET 開発者が頻繁に作成する代表的なコードを次に示します。

string conString =
  System.Configuration.ConfigurationManager.
    ConnectionStrings["Northwind"].ConnectionString;

これは少量のコードですが、複数のアプリケーションで接続文字列の名前だけを変えて繰り返し使用されるコードです。さらに、IntelliSense には接続文字列名のサポートがないので、コードでエラーが発生しやすくなります。実行時にのみ検出される、"スペル" エラーが発生することがあります (開発者がコードを入力しているときに、上司が肩越しに眺めているような場合によく発生します)。もう 1 つの例として、INotifyPropertyChanged の実装があります。開発者は各プロパティで "スペル" エラーを引き起こす可能性があります。

Northwind という接続文字列を取得するこのコードは、次に示すように、接続文字列ごとに ConnectionManager クラスを作成するコード生成ソリューションが存在するとさらに役に立ちます。

string conString = ConnectionManager.Northwind;

コード生成ソリューションを使用する状況がわかったら、次に、ソリューションから生成するコードのサンプルを記述します。この場合、ConnectionManager クラスは次のようになります。

public partial class ConnectManager
{
  public static string Northwind
    {
      get
        {
          return System.Configuration.ConfigurationManager.
            ConnectionStrings["Northwind"].ConnectionString;
        }
    }
}

このコードは、コード生成ソリューションの条件を満たしています。繰り返しのあるコードです。接続文字列ごとにプロパティ コードが繰り返されます。変更箇所はほんの少しです。接続文字列の名前とプロパティ名を変更するだけです。また、入力数が少なく、接続文字列の名前だけです。

初めてのコード生成テンプレート

T4 ソリューションは、"コード生成パッケージ" から構成できます。パッケージには、開発者がコード生成プロセスに挿入する入力を含むファイルと、これらの入力からコードを生成するテンプレート ファイルを含めます。どちらのファイルも T4 テンプレート ファイルで、アプリケーションの作成時に使用するのと同じプログラミング ツールを使用して作成します。このような設計により、コード生成テンプレートと、開発者がプロセスに入力を挿入するために使用するファイルとを分けることができます。

コード生成ソリューションの作成を開始するには、コードを生成する T4 テンプレート ファイルを、テスト対象のアプリケーションに追加する必要があります。できれば、ソリューションの使用を想定しているアプリケーションと似たアプリケーションをテスト対象にします。T4 テンプレート ファイルを追加するには、Visual Studio の [新しい項目の追加] ダイアログ ボックス (図 1 参照) で、[テキスト テンプレート] を追加し、コード生成テンプレート用の適切な名前を指定します (ConnectionManagerGenerator など)。お使いの Visual Studio のバージョンに [テキスト テンプレート] オプションがない場合は、新しい [テキスト ファイル] (これは [全般] セクションでも確認できます) を追加し、T4 処理をトリガーするためにファイルの拡張子を ".tt" にします。テキスト ファイルを追加すると、警告メッセージが表示されますが、無視しても問題ありません。

Adding a T4 Template
図 1 T4 テンプレートの追加

新しいテンプレート ファイルのプロパティを確認すると、[カスタム ツール] プロパティが TextTemplatingFileGenerator に設定されていることがわかります。このカスタム ツールは Visual Studio によって自動的に実行され、コード生成プロセスを管理するホストになります。T4 のテンプレート ファイルの内容は、コード生成ホストに渡されます。これにより、生成されるコードが、テンプレート ファイルの入れ子になっている子ファイルに書き込まれます。

プロジェクトにテキスト ファイルを追加していると、テンプレート ファイルは空になります。テンプレート ファイルを追加できた場合は、このファイルに <#@...#> 区切り記号を使って 2 つの T4 ディレクティブが含まれます (テキスト ファイルを追加している場合、これらのディレクティブを追加する必要があります)。これらのディレクティブは、テンプレートの記述に使用する言語 (生成されるコードの言語ではありません) と、子ファイルの拡張子を指定します。次の例では、2 つのディレクティブによって、テンプレートのプログラミング言語を Visual Basic に、生成されるコードを含む子ファイルのファイル拡張子を .generated.cs に設定しています。

<#@ template language="VB" #>
<#@ output extension=".generated.cs" #>

昔ながらの "Hello, World" アプリケーションを作成する場合は、次のようなコードをテンプレート ファイルに追加するだけです (テンプレートの記述に使用する言語が Visual Basic なのに対し、テンプレートには C# コードが生成されているのがわかります)。

public class HelloWorld
{
  public static string HelloWorld(string value)
  {
    return "Hello, " + value;
  }
}

このサンプルでは定型コードのみを使用しています。T4 の定型コードは、テンプレートからコード ファイルにそのままコピーされます。Visual Studio 2010 では、テンプレート ファイルから切り替えるときや、テンプレート ファイルを保存するときに定型コードがコピーされます。また、ソリューション エクスプローラーでテンプレート ファイルを右クリックして、コンテキスト メニューの [カスタム ツールの実行] をクリックするか、ソリューション エクスプローラーの上部で [すべてのテンプレートの変換] をクリックして、コード生成をトリガーすることもできます。

生成をトリガーして、テンプレートのコード ファイル (テンプレートの output ディレクティブで指定した拡張子のファイル) を開くと、テンプレートで指定したコードが含まれていることがわかります。Visual Studio では新しいコードのバックグラウンド コンパイルも行うため、生成されたコードをアプリケーションの残りの部分から使用することができます。

コードを生成する

ただし、定型コードだけでは不十分です。ConnectionManager ソリューションでは、アプリケーションに必要な接続文字列ごとにプロパティを動的に生成する必要があります。動的なコードを生成するには、コード生成プロセスを管理するコントロール コードと、コード生成ソリューションを使用して開発者から入力を受け取る変数を追加する必要があります。

ConnectionManager では、System.Collections 名前空間の ArrayList (以前は Connections と呼んでいました) を使用して、コード生成プロセスへの入力を形成する接続文字列のリストを保持します。テンプレート内のコードで使用する場合にこの名前空間をインポートするには、T4 の Import ディレクティブを使用します。

<#@ Import Namespace="System.Collections" #>

これで、生成されたクラスを開始する静的コードを追加できます。ここでは C# コードを生成しているため、ConnectionManager の初期コードは次のようになります。

public partial class ConnectionManager
{

次に、出力コードを動的に生成するコントロール コードを追加する必要があります。生成を制御するコード (子ファイルにコピーするのではなく、実行するコード) は、<#...#> 区切り記号で囲む必要があります。この例では、コントロール コードと生成されるコードを簡単に区別できるように、コントロール コードを Visual Basic で記述しています (これがコード生成プロセスの要件というわけではありません)。ConnectionManager ソリューションのコントロール コードは、次に示すように、接続文字列ごとに Connections コレクション全体をループ処理します。

<#
  For Each conName As String in Connections
#>

また、テンプレートのコントロール コードだけでなく、動的に生成されるコードに組み込まれる値を持つ式も追加する必要があります。ConnectionManager ソリューションでは、接続文字列の名前をプロパティの宣言および ConnectionStrings コレクションに渡すパラメーターに組み込む必要があります。式を評価し、式の値を定型コードに挿入するには、式を <#=…#> 区切り記号で囲む必要があります。次の例では、conName 変数の値を、For Each ループ内の静的コードの 2 つの場所に動的に挿入しています。

public static string <#= conName #>
{
  get
  {
    return System.Configuration.ConfigurationManager.
      ConnectionStrings["<#= conName #>"].ConnectionString;
  }
}
<#
  Next
#>
}

後は、接続文字列名のリストを保持する ArrayList を定義するだけです。これを行うには、T4 クラス機能を使用します。T4 クラス機能は、通常、ヘルパー関数を定義するために使用しますが、コード生成プロセス中に使用するフィールドなどのクラス レベルの項目の定義にも使用できます。クラス機能は、テンプレートの末尾に、次のように追加する必要があります。

<#+
  Dim Connections As New ArrayList()
#>

この T4 テンプレートは、ConnectionManager ソリューションの最初の部分を形成する、コード生成テンプレートです。次に、ソリューションの 2 つ目の部分を作成する必要があります。これは、開発者がコード生成プロセスに入力を挿入するために使用する入力ファイルです。

コード生成パッケージを使用する

開発者がコード生成プロセスに入力を挿入する場所を指定するには、コード生成ソリューションをテストするアプリケーションに 2 つ目の T4 テンプレートを追加します。このテンプレートには、コード生成テンプレートをこのテンプレートにコピーする Include ディレクティブを含める必要があります。コード生成テンプレート ファイルに ConnectionManagerGenerator と名前を付けたため、ConnectionManager ソリューションの入力テンプレート ファイルは次のようになります。

<#@ template language="VB" #>
<#@ output extension=".generated.cs" #>
<#@ Import Namespace="System.Collections" #>
<#
#>
<#@ Include file="ConnectionManagerGenerator.tt" #>

コード生成の実行時にホスト プロセスは、実際に、テンプレートに指定されたディレクティブ、コントロール コード、および静的コードから .NET の中間プログラムを組み立て、出来上がったプログラムを実行します。テンプレートの子ファイルに組み込まれるのは、この中間プログラムからの出力です。Include ディレクティブを使用すると、コード生成テンプレートが (Connections の ArrayList の宣言を使用して) このファイルの内容とマージされ、このような中間プログラムが作成されます。このソリューションを使用する開発者は、コード生成テンプレートで使用される変数を設定するこのテンプレートにコードを追加するだけです。このプロセスにより、開発者は使い慣れたプログラミング言語を使用して、コード生成への入力を指定することができます。

開発者が ConnectionManager ソリューションを使用する場合、アプリケーションの app.config ファイルまたは web.config ファイルにリストされている接続文字列の名前を、Connections ArrayList に追加する必要があります。これらの設定は、実行する必要があるコントロール コードの一部であるため、コードを <#...#> 区切り記号で囲む必要があります。また、開発者のコードは、変数を設定してからコードが実行されるように、Include ディレクティブの前に配置する必要があります。

Northwind および PHVIS という名前の 2 つの接続文字列の ConnectionManager を生成するには、開発者は次のコードを、入力テンプレートの Include ディレクティブより前に追加します。

<#
  Me.connections.Add("Northwind")
  Me.connections.Add("PHVIS")
#>
<#@ Include file="ConnectionManagerGenerator.tt" #>

これで、コード生成テンプレート ファイルと入力テンプレート ファイルから構成されるコード生成パッケージの完成です。このソリューションを使用する開発者は、両方のファイルを自身のアプリケーションにコピーし、入力ファイル内の変数を設定して入力ファイルを閉じるか保存して、ソリューション コードを生成する必要があります。企業のコード生成開発者は、コード生成パッケージを、2 つのテンプレート ファイルを含む Visual Studio 項目テンプレートとして設定できます。ConnectionManager ソリューションの適切な使用方法ではありませんが、開発者が異なる入力に基づいて、もう 1 つ別のコード セットを生成する必要がある場合、2 つ目の入力セットを保持するため、入力ファイルの 2 つ目のコピーを作成することになります。

このソリューションの構造には欠点が 1 つあります。このソリューションを使用するアプリケーションには、入力テンプレートとコード生成テンプレートの両方が含まれることになります。ConnectionManager ソリューションでは、Visual Studio によってコンパイルされるコードをこの両方のテンプレートから生成する場合、生成された 2 つのコード ファイルでは ConnectionManager という名前のクラスが定義され、アプリケーションがコンパイルされなくなります。これを回避する方法はたくさんありますが、コード生成テンプレートを変更して、生成されるコード ファイルの拡張子を Visual Studio が認識できない拡張子にするのが最も簡単な方法です。コード生成テンプレート ファイルの output ディレクティブを変更するのが効果的です。

<#@ output extension=".ttinclude" #>

コード生成ツールおよびリソース

MSDN ライブラリのページ以外に、T4 の使用方法について役立つ情報を提供しているのは、Oleg Sych のブログ (olegsych.com、英語) です。コード生成ソリューションの開発時には、彼の説明やツールを求めて必ずアクセスしました。彼の T4 ツールボックスには、T4 ソリューション開発のためのテンプレートがいくつか用意されています (単一の T4 テンプレートから複数の出力ファイルを生成するためのテンプレートや、コード生成プロセスを管理するためのツールなどがあります)。Sych のツールキットには、いくつかのコード生成シナリオ用のパッケージも含まれています。

Visual Studio では基本的に、T4 テンプレートをテキスト ファイルとして処理します。つまり、開発者がエディターに求める IntelliSense サポートや強調表示などの機能はすべて使用できません。Visual Studio 拡張機能マネージャーには、T4 による開発を支援するツールがいくつか用意されています。Clarius Consulting が提供する Visual T4 (bit.ly/maZFLm、英語) および Devart が提供する T4 Editor (bit.ly/wEVEVa、英語) を利用すると、エディターに必ず用意されている多くの機能を使用できます。また、Tangible Engineering が提供する T4 Editor (無料版または Pro Edition) を bit.ly/16jvGY (英語) で入手できます。この T4 Editor にはビジュアル デザイナーが用意されており、これを使用して、統一モデリング言語 (UML) に似たダイアグラムからコード生成パッケージを作成することができます (図 2 参照)。

The Default Editor for T4 Template Files (Left) Isn’t Much Better than NotePad—Adding the Tangible Editor (Right) Gives You the Kind of Features You Expect in Visual Studio
図 2 T4 テンプレート ファイルの既定のエディター (左) はメモ帳とそれほど変わらないが、Tangible Engineering が提供する T4 Editor を追加すると (右)、Visual Studio でおなじみの機能を利用できる

他のコード ベースのソリューションと同様に、最初はこのソリューションが機能するとは思えないでしょう。テンプレート ファイルのコンテキスト メニューの [カスタム ツールの実行] をクリックした後、テンプレートのコントロール コードで発生するコンパイル エラーは、[エラー一覧] に報告されます。ただし、コントロール コードがコンパイルされても、テンプレートの子ファイルには、ErrorGeneratingOutput という単語のみ表示され、後は空になっている場合があります。これは、コード生成パッケージのコントロール コードを実行すると、エラーが生成されることを示します。エラーの原因が判明しない場合は、このコントロール コードをデバッグする必要があります。

コード生成パッケージをデバッグするには、まず次のように、template ディレクティブの debug 属性を True に設定する必要があります。

<#@ template language="VB" debug="True"#>

これで、コントロール コードにブレーク ポイントが設定され、Visual Studio から認識できます。続いて、最も信頼性の高い方法でアプリケーションをデバックするのであれば、Visual Studio の 2 つ目のコピーを起動し、[デバッグ] メニューの [プロセスにアタッチ] をクリックします。表示されるダイアログ ボックスで、実行している devenv.exe のもう 1 つのコピーを選択し、[アタッチ] をクリックします。これで、Visual Studio の元のコピーに戻り、入力ファイルのコンテキスト メニューの [カスタム ツールの実行] を使用して、コードの実行を開始できます。

以上の手順を実行しても機能しない場合は、テンプレートのコントロール コードに次の行を挿入して、デバッグをトリガーできます。

System.Diagnostics.Debugger.Break()

Windows 7 にインストールされている Visual Studio 2010 を使用している場合は、Break メソッドを呼び出す前に次の行を追加します。

System.Diagnostics.Debugger.Launch()

テンプレート コードを実行すると、この行によって、Visual Studio が再起動されるか、デバッグを実行できるダイアログ ボックスが表示されます。デバッグ オプションを選択すると、テンプレート コードを実行するプロセスに既にアタッチされている Visual Studio の 2 つ目のコピーが起動します。Visual Studio の最初のインスタンスは、コードのデバッグ中に無効になります。残念ながら、デバッグ セッションが完了しても Visual Studio は無効なモードのままになります。これを回避するには、Windows のレジストリで Visual Studio の設定の 1 つを変更する必要があります (詳細については、T4 のデバッグに関する Sych のブログ記事 (bit.ly/aXJwPx、英語) を参照してください)。また、問題を修正したら、このステートメントを削除するのを忘れないでください。

もちろん、このソリューションは、開発者が接続文字列の名前を入力ファイルに正しく入力することを前提としています。アプリケーションの構成ファイルから接続文字列を読み取るコードが ConnectionManager に含まれており、開発者による入力の挿入を必要としないのが、より優れたソリューションです。残念ながら、コードは実行時ではなくデザイン時に生成されるため、ConnectionManager を使用して構成ファイルを読み取ることはできません。また、構成ファイルを処理するには、System.XML クラスを使用する必要があります。これらのクラスを適用するには、前に System.Collections から ArrayList を取得したときのように、assembly ディレクティブを追加する必要があります。また、assembly ディレクティブの name 属性に DLL のフルパスを設定して、固有のカスタム ライブラリに参照を追加することもできます。

<#@ assembly name="C:\PHVIS\GenerationUtilities.dll" #>

以上の処理は、ツールキットにコード生成を追加して、生産性を向上し、コードの品質や信頼性を高めるために不可欠です。T4 では、使い慣れたツールセットを使用してコード生成ソリューションを作成できるので、簡単に作業に取り掛かることができます。T4 を使用するにあたって、これらのツールを適用すべき状況を把握できるようになることが最大の課題です。

Peter Vogel は PH&V Information Services の社長です。彼の最新の著書は『Practical Code Generation in .NET』 (Addison-Wesley Professional、2010 年) です。PH&V Information Services は、サービス ベース アーキテクチャの設計の促進、およびこれらのアーキテクチャへの .NET テクノロジの統合に特化しています。Vogel はコンサルティング業務以外にも、世界中に受講者がいる Learning Tree International 用の SOA 設計コースを執筆しています。

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