特集
.NET開発者のためのDI&AOP入門(後編)

コードで簡単に分かる“AOP”

株式会社アイビス 杉本 和也
2008/01/23
Page1 Page2

3-2. AOPの実践

 今回はメソッドの開始時と終了時にコンソールにログを出力するサンプルを作成する。

Interceptorクラスの作成

 まず「Interceptor」と呼ばれる種類のクラスを作成する。Interceptorクラスには複数のメソッドから抽出した共通処理を記述する。この共通処理は、後から実行時に各メソッドに注入(Injection)されることになる。今回の場合はロギング処理を行う「ConsoleTraceInterceptor」という名前のInterceptorクラスを作成する。

 Interceptorクラスは、IMethodInterceptorインターフェイス(Seasar.Framework.Aop名前空間)を実装して作成する。Aspect属性を記述したメソッド(本稿の例ではGetEmpNameWithTitleメソッド。以下、「対象メソッド」)が呼び出されると、そのAspect属性のパラメータに指定したInterceptorクラスのインターフェイスであるIMethodInterceotorのInvokeメソッドが実行される。

 Invokeメソッドのパラメータには、IMethodInvocationインターフェイス(Seasar.Framework.Aop名前空間)のオブジェクト「invocation」が引き渡される。invocationオブジェクトは対象メソッドのパラメータの値など、対象メソッドに関する情報を持っている。

 invocationオブジェクトの型であるIMethodInvocationインターフェイスの主なプロパティ・メソッドの役割は以下のとおりである。

  • Methodプロパティ:対象メソッドのメタデータを取得できる
  • Argumentsプロパティ:対象メソッドに渡されたパラメータの値を取得できる(Object型の配列)
  • Proceedメソッド:対象メソッドの処理を実行できる(戻り値は本来のメソッドの戻り値)

 以下は、Interceptorクラスの実装例である。Invokeメソッドの戻り値が、対象メソッドの戻り値となる。

using System;
using Seasar.Framework.Aop;

namespace QuillSample
{
  public class ConsoleTraceInterceptor : IMethodInterceptor
  {
    #region IMethodInterceptorメンバ

    public object Invoke(IMethodInvocation invocation)
    {
      // 対象メソッドの完全名
      string methodName =
        invocation.Method.DeclaringType.FullName
        + "." + invocation.Method.Name;

      // メソッド開始のログをコンソールに出力する
      Console.WriteLine("開始" + methodName);

      // 対象メソッドが持つ本来の処理を実行する
      object ret = invocation.Proceed();

      // メソッド終了のログをコンソールに出力する
      Console.WriteLine("終了" + methodName);
     
      // 戻り値を返す
      return ret;
    }

    #endregion
  }
}
リスト3 IMethodInterceptorインターフェイスを実装したInterceptorクラス(ConsoleTraceInterceptor.cs)

 参考までに、対象メソッドのパラメータや戻り値などの情報をログに出力するように作り込むと、以下のようなソース・コードになる(本稿ではこのリスト3の結果を実行結果例に利用している)。

using System;
using System.Text;
using Seasar.Framework.Aop;
using System.Reflection;

namespace QuillSample
{
  public class ConsoleTraceInterceptor : IMethodInterceptor
  {
    #region IMethodInterceptor メンバ

    public object Invoke(IMethodInvocation invocation)
    {
      MethodBase mb = invocation.Method;

      // StringBuilderのバッファの初期容量を標準(16)より
      // 多くして、メモリの割り当て回数を減らす
      StringBuilder buf = new StringBuilder(100);

      // 対象メソッドの完全名
      buf.Append(mb.DeclaringType.FullName);
      buf.Append(".");
      buf.Append(mb.Name);
      buf.Append("(");

      // 対象メソッドに渡されたパラメータの内容
      object[] args = invocation.Arguments;
      if (args != null && args.Length > 0)
      {
        for (int i = 0; i < args.Length; i++)
        {
          buf.Append(args[i]);
          buf.Append(", ");
        }
        buf.Length = buf.Length - 2;
      }

      buf.Append(")");

      // メソッド開始のログをコンソールに出力する
      Console.WriteLine("開始" + buf.ToString());

      object ret = null;
      Exception cause = null;
     
      try
      {
        // 本来の処理を実行する
        ret = invocation.Proceed();
        buf.Append(" : ");
        buf.Append(ret);
      }
      catch (Exception ex)
      {
        buf.Append(" 例外:");
        buf.Append(ex);
        cause = ex;
      }

      // メソッド終了のログをコンソールに出力する
      Console.WriteLine("終了" + buf.ToString());
     
      if (cause == null)
      {
        return ret;
      }
      else
      {
        throw cause;
      }
    }

    #endregion
  }
}
リスト4 対象メソッドのパラメータや戻り値などの情報をログに出力するInterceptorクラス(ConsoleTraceInterceptor.cs)

Aspect属性の作成

 前述したように、対象メソッドにAspect属性を設定することで、Interceptorクラスを適用するメソッドを指定することができる。なお、Aspect属性はメソッドだけではなく、そのクラスに指定して、クラスの全メソッドにまとめてInterceptorクラスを適用することもできる。

 ここでは例として、EmployeeLogicクラスのGetEmpNameWithTitleメソッドとEmployeeDaoクラスのGetEmpNameメソッドにAspect属性を設定してログを出力してみよう。なお今回は、Interceptorクラスによってメソッド実行の前後にログ出力が行われていることが分かりやすいように、メソッド中にもログ出力のコードを記述しておく。

 対象メソッドへのAspect属性の指定で注意しなくてはならないのが、対象メソッドに「virtualキーワード」を付けなくてはいけないことである。実際にはインターフェイスを介して対象メソッドが呼び出される場合には必要ないのだが、インターフェイスを介せずに対象メソッドを呼び出す場合にはvirtualキーワードを付けておかないとAspect属性が有効にならない。これはQuillが型の拡張によりAOPを実現しているための制限である。このため静的メソッドのようなvirtualキーワードを付けられないメソッドにはAspect属性を付けることはできない。

 次のコードは、EmployeeLogicクラスのGetEmpNameWithTitleメソッドとEmployeeDaoクラスのGetEmpNameメソッドの実装例だ。

using System;
using Seasar.Quill.Attrs;

namespace QuillSample
{
  public class EmployeeLogic : IEmployeeLogic
  {
    public IEmployeeDao EmpDao;

    #region IEmployeeLogic メンバ

    [Aspect(typeof(ConsoleTraceInterceptor))]
    public virtual string GetEmpNameWithTitle(int empNo)
    {
      Console.WriteLine(
        "実行QuillSample.EmployeeLogic.GetEmpNameWithTitle");
      string empName = EmpDao.GetEmpName(empNo) + "さん";
      return empName;
    }

    #endregion
  }
}
リスト5 Aspect属性をメソッドに指定したコード1(EmployeeLogic.cs)

using System;
using Seasar.Quill.Attrs;

namespace QuillSample
{
  public class EmployeeDao : IEmployeeDao
  {
    #region IEmployeeDaoメンバ

    [Aspect(typeof(ConsoleTraceInterceptor))]
    public virtual string GetEmpName(int empNo)
    {
      Console.WriteLine(
        "実行QuillSample.EmployeeDao.GetEmpName");

      // 本来はデータベースなどから取得するがDIが目的なので
      // パラメータの社員コードにかかわらず「山田太郎」固定で返
      return "山田太郎";
    }

    #endregion
  }
}
リスト6 Aspect属性をメソッドに指定したコード2(EmployeeDao.cs)

 それではデバッグ・モードで実行してみよう。

本稿で作成したサンプル・アプリケーション

 [社員コードを入力してください]に数字を入力して[社員検索]ボタンをクリックすると、以下のようなログが[出力]ウィンドウに出力されているはずである。なお、次の出力は[社員コードを入力してください]に「3」を入力した場合であり、ConsoleTraceInterceptor.csファイルのコードは前述のリスト4を利用している。

開始 QuillSample.EmployeeLogic.GetEmpNameWithTitle(3)
実行 QuillSample.EmployeeLogic.GetEmpNameWithTitle
開始 QuillSample.EmployeeDao.GetEmpName(3)
実行 QuillSample.EmployeeDao.GetEmpName
終了 QuillSample.EmployeeDao.GetEmpName(3) : 山田太郎
終了 QuillSample.EmployeeLogic.GetEmpNameWithTitle(3) : 山田太郎さん
QuillによるAOPサンプルの実行結果

 想定どおりにログ出力されたことが確認できたであろうか。このようにAOPを適用することで、本来の目的ではない「業務処理以外のコード」が分離され、本来の目的である業務処理だけのシンプルなソース・コードにすることができる。

 ただし、AOPはとても有効な手段であるがデバッグが行いにくい欠点もあるので、多用は厳禁である。実際に1行ずつステップ実行していただくと分かるが、AOPを用いたメソッドを呼び出すと、ステップがInterceptorクラスのInvokeメソッドと、対象のメソッドとを行き来することになる。

 前回説明したように、Seasar.NETには、AOPをサポートしたDIコンテナである「S2Container.NET」フレームワークと、O/Rマッピング・フレームワークである「S2Dao.NET」フレームワークが用意されている。そして、S2Container.NETフレームワークでは、「Quill」と「S2Container」という2種類のDIコンテナが提供されている。本特集ではよりシンプルなQuillでAOPを説明したが、S2Containerではトランザクション処理を行うInterceptorクラスなども標準で用意されている。

 トランザクション処理についてはQuillでも近く標準で用意される予定だが、いまのところ(バージョン1.3.6)はまだ用意されていない。

 現状で、QuillによりAOPを用いたトランザクション処理を行いたい場合には、S2Containerと連携する必要がある。S2Container.NETフレームワークの配布物の中にはQuillのサンプル・アプリケーション(Seasarソリューションに含まれるSeasar.Quill.Examplesプロジェクト)が含まれており、そこでトランザクション処理を行うInterceptorクラスの使用方法やS2Dao.NETフレームワークによるデータベース・アクセスの例を確認することができる。今回作成したサンプルとよく似た構成になっており、また、データベースにはAccessを使用しているため、すぐにテスト実行できるので、ぜひ参照してほしい。

4. まとめ

 以上でS2Container.NETのQuillによるDIとAOPについての紹介を終える。DIやAOPというキーワードの言葉だけの説明を見ると難しく感じるが、ソース・コードを実際に書いて動かしてみることで有効性や可能性をより感じていただけたのではないかと思う。

 補足であるが、本特集の前編ではDIの構成が分かりやすくなるように、インターフェイスを作成して、その機能を実装するクラスを作成した。しかし業務ロジックの場合はインターフェイスとクラスの関係が1対1の場合が多い。そのような場合、インターフェイスは作成せずにクラスだけを作成しておき、後に必要となった際にインターフェイスを作成するという考え方もある。筆者もそれでよいと思っている。インターフェイスを常に作成するかどうかはプロジェクトに合わせて検討すればよいだろう。

 .NET Frameworkの技術者はJavaの技術者に比べるとオープンソース・ソフトウェアに触れる機会が少ないように思う。本稿をきっかけに、DIやAOPはもちろん、Seasar.NETプロジェクトをはじめオープンソース・ソフトウェアの利用や開発に興味を持っていただければうれしい。End of Article

 

 INDEX
  [特集].NET開発者のためのDI&AOP入門
  Seasar.NETでDIを始めよう
    1.DI&AOP開発環境の準備
    2.DIとAOPを理解するためのサンプルの作成
    3.ソース・コードで理解するDI
    4.DIが役立つ場面
 
  コードで簡単に分かる“AOP”
    1.ソース・コードで理解するAOP
  2.AOPの実践


Insider.NET フォーラム 新着記事
  • 第2回 簡潔なコーディングのために (2017/7/26)
     ラムダ式で記述できるメンバの増加、throw式、out変数、タプルなど、C# 7には以前よりもコードを簡潔に記述できるような機能が導入されている
  • 第1回 Visual Studio Codeデバッグの基礎知識 (2017/7/21)
     Node.jsプログラムをデバッグしながら、Visual Studio Codeに統合されているデバッグ機能の基本の「キ」をマスターしよう
  • 第1回 明瞭なコーディングのために (2017/7/19)
     C# 7で追加された新機能の中から、「数値リテラル構文の改善」と「ローカル関数」を紹介する。これらは分かりやすいコードを記述するのに使える
  • Presentation Translator (2017/7/18)
     Presentation TranslatorはPowerPoint用のアドイン。プレゼンテーション時の字幕の付加や、多言語での質疑応答、スライドの翻訳を行える
@ITメールマガジン 新着情報やスタッフのコラムがメールで届きます(無料)

注目のテーマ

Insider.NET 記事ランキング

本日 月間