アスペクト指向プログラミング

RealProxy クラスによるアスペクト指向プログラミング

Bruno Sonnino

適切に設計されたアプリケーションは独立した複数の層に分けられ、各層が異なる処理を担当し、必要以上にやり取りを行いません。疎結合型の保守しやすいアプリケーションを設計しているのに、開発も中盤に差し掛かったところでアーキテクチャにうまく適合しない次のような要件が提示されたとします。

  • データベースへの書き込みを行う前にデータを検証する必要がある。
  • 重要な操作には監査やログ記録が必要である。
  • 操作に問題がないかをチェックするためにデバッグ ログを管理する必要がある。
  • 一部の処理のパフォーマンスを測定して、パフォーマンスが目的の範囲内に収まっているかどうかを確認する必要がある。

これらの要件はどれも、多くの作業が必要です。そのうえ、コードの重複が生じます。同じコードをシステムの多くの箇所に追加しなければならず、DRY (don’t repeat yourself: 同じことを繰り返さない) の原則に反し、保守が難しくなります。要件の変更は、プログラムの大幅な変更につながります。アプリケーションにこのような修正を加える必要があると、個人的には「複数の箇所で繰り返されるコードをコンパイラが自動的に追加できないはなぜだろう」、あるいは「[このメソッドにログ記録を追加する] オプションがあったらよいのに」と思います。

このようなことを実現するのがアスペクト指向プログラミング (AOP) です。AOP は、一般的なコードと、アスペクト (オブジェクトや層の境界を横断する機能) を分離します。たとえば、アプリケーション ログはどのアプリケーション層にも結び付けられません。プログラム全体に適用され、どこにでも存在します。このようなものを「横断的な懸念事項」と呼びます。

AOP とは「横断的な懸念事項の分離を可能にすることによって、モジュール性を高めることを目的としたプログラミング パラダイム」です (Wikipedia からの引用、英語)。AOP はシステムの複数箇所で行われる機能を取り出し、アプリケーションの中核から分離します。そして、このような懸念事項の分離を進めながら、コードの重複や結合を取り除きます。

今回は、AOP の基礎を紹介し、Microsoft .NET Framework の RealProxy クラスによる動的プロキシを使用して、AOP の実現を容易にする方法を詳しく説明します。

AOP を実装する

AOP の最も大きなメリットは、1 か所のアスペクトだけに注意を向ければよいことです。アスペクトをプログラミングすれば、必要な箇所すべてに適用されます。AOP には次のような用途があります。

  • アプリケーションにログ記録を実装する。
  • 操作の前に認証を行う (操作によっては認証済みのユーザーだけに許可するなど)。
  • プロパティの set アクセス操作子に検証や通知を実装する (INotifyPropertyChanged インターフェイスを実装するクラスのプロパティが変化したときに PropertyChanged イベントを呼び出す)。
  • あるメソッドの動作を変更する。

このように、AOP には多くの用途がありますが、扱いは慎重にする必要があります。AOP は開発者が感知しないコードを保持しますが、感知しなくても存在しているため、アスペクトが存在する箇所での呼び出しのたびにそのコードが実行されます。これは、バグを生じさせたり、アプリケーションのパフォーマンスに重大な影響を及ぼすおそれがあります。アスペクトのささいなバグがデバッグ時間の浪費につながることもあります。アスペクトを多くの箇所で使用しないのであれば、直接コードに追加した方が適切な場合もあります。

AOP の実装によく使われるテクニックがいくつかあります。

  • プリプロセッサを使用してソース コードを追加する (C++ のプリプロセッサなど)。
  • ポストプロセッサを使用してコンパイル後のバイナリ コードに命令を追加する。
  • 特殊なコンパイラを使用してコンパイル時にコードを追加する。
  • 実行時にコード インターセプターを使用して実行をインターセプトして目的のコードを追加する。

これらのテクニックの中で .NET Framework で最も一般的に使用されるのは、ポストプロセッサによる処理とコードのインターセプトです。前者は PostSharp (postsharp.net、英語) で使用されているテクニックで、後者は Castle DynamicProxy (bit.ly/JzE631、英語) や Unity (unity.codeplex.com、英語) などの依存関係挿入 (DI) コンテナーで使用されるテクニックです。これらのツールでは、コードのインターセプトを行うために、通常、Decorator や Proxy といったデザイン パターンが使用されます。

Decorator デザイン パターン

Decorator デザイン パターンは、開発済みのクラスに機能を追加するというよくある問題を解決します。このパターンにはいくつか選択肢があります。

  • 新しい機能をクラスに直接追加する。ただし、クラスに別の責任が生じるため、「単一責任」の原則に違反します。
  • 必要な機能を実行する新しいクラスを作成し、元のクラスから呼び出す。これは新たな問題をもたらします。新しく追加される機能を必要としないクラスがある場合はどうすればよいでしょう。
  • 元のクラスから新しいクラスを継承して新しい機能を追加する。ただし、新しいクラスが数多く作成されることになります。たとえば、作成、読み取り、更新、削除 (CRUD) のデータベース操作を行うリポジトリ クラスがあるとします。ここに監査機能を追加することになりました。その後、データが正しく更新されるようにデータ検証も追加します。さらに、認証済みのユーザーだけがクラスにアクセスできるようにアクセス認証も必要になりました。これは大きな問題です。3 つのアスペクトすべてを実装するクラスもあれば、2 つだけ実装するクラスや 1 つだけ実装するクラスもあるでしょう。最終的にはクラスはいくつになるのでしょう。
  • クラスをアスペクトで "装飾" し、そのアスペクトを使用する新しいクラスを作成して、元のクラスを呼び出す。この場合、アスペクトが 1 つ必要であれば装飾を 1 回行い、2 つ必要なら 2 回装飾を行います。おもちゃを注文するとします (もちろん、Xbox やスマートフォンでもかまいません)。おもちゃには店での陳列や保護のために箱が必要です。ギフト包装するよう注文を受けたら、2 つ目の装飾として、ギフト カードを添え、ギフト用の包装紙で包み、リボンをかけて箱を装飾します。店舗からの発送時、さらに 3 つ目の箱に入れ、緩衝材として発泡スチロールを詰めます。この 3 つの装飾はそれぞれ異なる機能があり、互いに独立しています。ギフト包装しないで購入することも、緩衝材を詰めた箱なしで店舗で直接受け取ることも、すべての箱を使わずに (特別割引で) 購入することもできます。どのような装飾の組み合わせでもおもちゃは手に入りますが、おもちゃの基本的な機能に変化はありません。

Decorator パターンの紹介はこのぐらいにして、ここからは C# での実装方法を示します。

まず、IRepository<T> インターフェイスを作成します。

 

public interface IRepository<T>
{
  void Add(T entity);
  void Delete(T entity);
  void Update(T entity);
  IEnumerable<T> GetAll();
  T GetById(int id);
}

このインターフェイスを Repository<T> クラスを使って実装します (図 1 参照)。

図 1 Repository<T> クラス

public class Repository<T> : IRepository<T>
{
  public void Add(T entity)
  {
    Console.WriteLine("Adding {0}", entity);
  }
  public void Delete(T entity)
  {
    Console.WriteLine("Deleting {0}", entity);
  }
  public void Update(T entity)
  {
    Console.WriteLine("Updating {0}", entity);
  }
  public IEnumerable<T> GetAll()
  {
    Console.WriteLine("Getting entities");
    return null;
  }
  public T GetById(int id)
  {
    Console.WriteLine("Getting entity {0}", id);
    return default(T);
  }
}

この Repository<T> クラスを使用して、以下の Customer クラスの要素の追加、更新、削除、および取得を行います。

public class Customer
{
  public int Id { get; set; }
  public string Name { get; set; }
  public string Address { get; set; }
}

プログラムは図 2 のようになります。

図 2 ログ記録なしのメイン プログラム

static void Main(string[] args)
{
  Console.WriteLine("***\r\n Begin program - no logging\r\n");
  IRepository<Customer> customerRepository =
    new Repository<Customer>();
  var customer = new Customer
  {
    Id = 1,
    Name = "Customer 1",
    Address = "Address 1"
  };
  customerRepository.Add(customer);
  customerRepository.Update(customer);
  customerRepository.Delete(customer);
  Console.WriteLine("\r\nEnd program - no logging\r\n***");
  Console.ReadLine();
}

このコードを実行すると、図 3 のように表示されます。

Output of the Program with No Logging
図 3 ログ記録なしプログラムの出力

上司からこのクラスにログ記録を追加するように求められたとします。このような場合、IRepository<T> を装飾する新しいクラスを作成します。この新しいクラスはビルドするためにクラスを受け取り、同じインターフェイスを実装します (図 4 参照)。

図 4 ログ記録リポジトリ

public class LoggerRepository<T> : IRepository<T>
{
  private readonly IRepository<T> _decorated;
  public LoggerRepository(IRepository<T> decorated)
  {
    _decorated = decorated;
  }
  private void Log(string msg, object arg = null)
  {
    Console.ForegroundColor = ConsoleColor.Red;
    Console.WriteLine(msg, arg);
    Console.ResetColor();
  }
  public void Add(T entity)
  {
    Log("In decorator - Before Adding {0}", entity);
    _decorated.Add(entity);
    Log("In decorator - After Adding {0}", entity);
  }
  public void Delete(T entity)
  {
    Log("In decorator - Before Deleting {0}", entity);
    _decorated.Delete(entity);
    Log("In decorator - After Deleting {0}", entity);
  }
  public void Update(T entity)
  {
    Log("In decorator - Before Updating {0}", entity);
    _decorated.Update(entity);
    Log("In decorator - After Updating {0}", entity);
  }
  public IEnumerable<T> GetAll()
  {
    Log("In decorator - Before Getting Entities");
    var result = _decorated.GetAll();
    Log("In decorator - After Getting Entities");
    return result;
  }
  public T GetById(int id)
  {
    Log("In decorator - Before Getting Entity {0}", id);
    var result = _decorated.GetById(id);
    Log("In decorator - After Getting Entity {0}", id);
    return result;
  }
}

この新しいクラスは、装飾するクラスのメソッドをラップし、ログ機能を追加します。ログ記録クラスを呼び出すには、メインのコードを少し変更する必要があります (図 5 参照)。

図 5 ログ記録リポジトリを使用するメイン プログラム

static void Main(string[] args)
{
  Console.WriteLine("***\r\n Begin program - logging with decorator\r\n");
  // IRepository<Customer> customerRepository =
  //   new Repository<Customer>();
  IRepository<Customer> customerRepository =
    new LoggerRepository<Customer>(new Repository<Customer>());
  var customer = new Customer
  {
    Id = 1,
    Name = "Customer 1",
    Address = "Address 1"
  };
  customerRepository.Add(customer);
  customerRepository.Update(customer);
  customerRepository.Delete(customer);
  Console.WriteLine("\r\nEnd program - logging with decorator\r\n***");
  Console.ReadLine();
}

単純に新しいクラスを作成して、コンストラクタのパラメーターとして元のクラスのインスタンスを渡します。プログラムを実行すると、ログ記録が行われるのがわかります (図 6 参照)。

Execution of the Logging Program with a Decorator
図 6 Decorator 付きのログ記録プログラムの実行

「わかった。アイデアは面白いけど、作業が面倒だ。クラスをすべて実装し、すべてのメソッドにアスペクトを追加しなければならないし、おそらくメンテナンスが難しくなる。別の方法はないだろうか」と思われたかもしれませんね。.NET Framework では、リフレクションを使えば、すべてのメソッドを取得して実行することができます。基本クラス ライブラリ (BCL) にも、自動的にこの実装を行う RealProxy クラス (bit.ly/18MfxWo、英語) があります。

RealProxy を使って動的プロキシを作成する

RealProxy クラスは、プロキシとしての基本機能を提供します。RealProxy は抽象クラスなので、クラスを継承し、Invoke メソッドをオーバーライドして新しい機能を追加する必要があります。このクラスは System.Runtime.Remoting.Proxies 名前空間に存在します。動的プロキシを作成するには、図 7 に示すようなコードを使用します。

図 7 動的プロキシ クラス

class DynamicProxy<T> : RealProxy
{
  private readonly T _decorated;
  public DynamicProxy(T decorated)
    : base(typeof(T))
  {
    _decorated = decorated;
  }
  private void Log(string msg, object arg = null)
  {
    Console.ForegroundColor = ConsoleColor.Red;
    Console.WriteLine(msg, arg);
    Console.ResetColor();
  }
  public override IMessage Invoke(IMessage msg)
  {
    var methodCall = msg as IMethodCallMessage;
    var methodInfo = methodCall.MethodBase as MethodInfo;
    Log("In Dynamic Proxy - Before executing '{0}'",
      methodCall.MethodName);
    try
    {
      var result = methodInfo.Invoke(_decorated, methodCall.InArgs);
      Log("In Dynamic Proxy - After executing '{0}' ",
        methodCall.MethodName);
      return new ReturnMessage(result, null, 0,
        methodCall.LogicalCallContext, methodCall);
    }
    catch (Exception e)
    {
     Log(string.Format(
       "In Dynamic Proxy- Exception {0} executing '{1}'", e),
       methodCall.MethodName);
     return new ReturnMessage(e, methodCall);
    }
  }
}

クラスのコンストラクタでは、装飾するクラスの型を渡して、基本クラスのコンストラクタを呼び出す必要があります。次に、IMessage パラメーターを受け取る Invoke メソッドをオーバーライドします。IMessage パラメーターには、Invoke メソッドに渡すすべてのパラメーターを含むディクショナリを 1 つ含めます。IMessage パラメーターを IMethodCallMessage に型キャストしているので、(MethodInfo 型の) MethodBase パラメーターを抽出できます。

次の手順として、メソッド呼び出し前のアスペクトを追加し、methodInfo.Invoke を使って本来のメソッドを呼び出してから、呼び出し後のアスペクトを追加します。

DynamicProxy<T> は IRepository<Customer> ではないため、プロキシを直接呼び出すことはできません。つまり、次のような呼び出しはできません。

IRepository<Customer> customerRepository =
  new DynamicProxy<IRepository<Customer>>(
  new Repository<Customer>());

装飾したリポジトリを使用するには、IRepository<Customer> のインスタンスを返す GetTransparentProxy メソッドを使用する必要があります。このインスタンスのすべてのメソッドは、プロキシの Invoke メソッド経由で呼び出されます。このプロセスを容易にするため、プロキシを作成してリポジトリのインスタンスを返すファクトリ クラスを作成します。

public class RepositoryFactory
{
  public static IRepository<T> Create<T>()
  {
    var repository = new Repository<T>();
    var dynamicProxy = new DynamicProxy<IRepository<T>>(repository);
    return dynamicProxy.GetTransparentProxy() as IRepository<T>;
  }
}

この場合、メイン プログラムは図 8 のようになります。

図 8 動的プロキシを利用するメイン プログラム

static void Main(string[] args)
{
  Console.WriteLine("***\r\n Begin program - logging with dynamic proxy\r\n");
  // IRepository<Customer> customerRepository =
  //   new Repository<Customer>();
  // IRepository<Customer> customerRepository =
  //   new LoggerRepository<Customer>(new Repository<Customer>());
  IRepository<Customer> customerRepository =
    RepositoryFactory.Create<Customer>();
  var customer = new Customer
  {
    Id = 1,
    Name = "Customer 1",
    Address = "Address 1"
   ;
  customerRepository.Add(customer);
  customerRepository.Update(customer);
  customerRepository.Delete(customer);
  Console.WriteLine("\r\nEnd program - logging with dynamic proxy\r\n***");
  Console.ReadLine();
}

このプログラムを実行すると、先ほどと同様の結果になります (図 9 参照)。

Program Execution with Dynamic Proxy図 9 動的プロキシを利用するプログラムの実行

これで、コードにアスペクトを追加できる動的プロキシの作成が完了しました。同じコードを繰り返す必要はなくなりました。新しいアスペクトを追加する場合は、RealProxy から継承して新しいクラスを作成し、それを使用して最初のプロキシを装飾するだけです。

上司が戻ってきて、コードに認証を追加して、管理者だけがリポジトリにアクセスできるようにするよう求められた場合、図 10 に示す新しいプロキシを作成できます。

図 10 認証プロキシ

 

class AuthenticationProxy<T> : RealProxy
{
  private readonly T _decorated;
  public AuthenticationProxy(T decorated)
    : base(typeof(T))
  {
    _decorated = decorated;
  }
  private void Log(string msg, object arg = null)
  {
    Console.ForegroundColor = ConsoleColor.Green;
    Console.WriteLine(msg, arg);
    Console.ResetColor();
  }
  public override IMessage Invoke(IMessage msg)
  {
    var methodCall = msg as IMethodCallMessage;
    var methodInfo = methodCall.MethodBase as MethodInfo;
    if (Thread.CurrentPrincipal.IsInRole("ADMIN"))
    {
      try
      {
        Log("User authenticated - You can execute '{0}' ",
          methodCall.MethodName);
        var result = methodInfo.Invoke(_decorated, methodCall.InArgs);
        return new ReturnMessage(result, null, 0,
          methodCall.LogicalCallContext, methodCall);
      }
      catch (Exception e)
      {
        Log(string.Format(
          "User authenticated - Exception {0} executing '{1}'", e),
          methodCall.MethodName);
        return new ReturnMessage(e, methodCall);
      }
    }
    Log("User not authenticated - You can't execute '{0}' ",
      methodCall.MethodName);
    return new ReturnMessage(null, null, 0,
      methodCall.LogicalCallContext, methodCall);
  }
}

両方のプロキシを呼び出すために、リポジトリ ファクトリを変更する必要があります (図 11 参照)。

図 11 2 つのプロキシで装飾されたリポジトリ ファクトリ

public class RepositoryFactory
{
  public static IRepository<T> Create<T>()
  {
    var repository = new Repository<T>();
    var decoratedRepository =
      (IRepository<T>)new DynamicProxy<IRepository<T>>(
      repository).GetTransparentProxy();
    // Create a dynamic proxy for the class already decorated
    decoratedRepository =
      (IRepository<T>)new AuthenticationProxy<IRepository<T>>(
      decoratedRepository).GetTransparentProxy();
    return decoratedRepository;
  }
}

メイン プログラムを図 12 に変更して実行すると、図 13 に示す結果が表れます。

図 12 2 人のユーザーがリポジトリを呼び出すメイン プログラム

static void Main(string[] args)
{
  Console.WriteLine(
    "***\r\n Begin program - logging and authentication\r\n");
  Console.WriteLine("\r\nRunning as admin");
  Thread.CurrentPrincipal =
    new GenericPrincipal(new GenericIdentity("Administrator"),
    new[] { "ADMIN" });
  IRepository<Customer> customerRepository =
    RepositoryFactory.Create<Customer>();
  var customer = new Customer
  {
    Id = 1,
    Name = "Customer 1",
    Address = "Address 1"
  };
  customerRepository.Add(customer);
  customerRepository.Update(customer);
  customerRepository.Delete(customer);
  Console.WriteLine("\r\nRunning as user");
  Thread.CurrentPrincipal =
    new GenericPrincipal(new GenericIdentity("NormalUser"),
    new string[] { });
  customerRepository.Add(customer);
  customerRepository.Update(customer);
  customerRepository.Delete(customer);
  Console.WriteLine(
    "\r\nEnd program - logging and authentication\r\n***");
  Console.ReadLine();
}

Output of the Program Using Two Proxies
図 13 2 つのプロキシを使用するプログラムの出力

このプログラムではリポジトリ メソッドを 2 回実行します。1 回目は管理ユーザーとして実行します。この場合、プロキシ メソッドは呼び出されます。2 回目は通常ユーザーとして実行します。この場合、プロキシ メソッドはスキップされます。

実に簡単ですね。ファクトリは IRepository<T> のインスタンスを返すので、装飾バージョンを使用しているかどうかはプログラムにはわかりません。これはリスコフの置換原則に従っています。リスコフの置換原則は、S 型が T 型のサブタイプであれば、T 型のオブジェクトは S 型のオブジェクトに置き換えることができるというものです。今回の場合は、IRepository<Customer> インターフェイスを使用することで、このインターフェイスを実装するあらゆるクラスを、変更を加えずにプログラムで使用できます。

フィルター関数

ここまでは、関数でフィルター処理を行っていません。アスペクトは呼び出したすべてのクラス メソッドに適用されます。多くの場合、これは望ましい動作ではありません。たとえば、取得メソッド (GetAll と GetById) にはログ記録は必要ないかもしれません。このような選別を実現する 1 つの方法は、アスペクトを名前でフィルター選択する方法です (図 14 参照)。

図 14 アスペクトのメソッドのフィルター選択

public override IMessage Invoke(IMessage msg)
{
  var methodCall = msg as IMethodCallMessage;
  var methodInfo = methodCall.MethodBase as MethodInfo;
  if (!methodInfo.Name.StartsWith("Get"))
    Log("In Dynamic Proxy - Before executing '{0}'",
      methodCall.MethodName);
  try
  {
    var result = methodInfo.Invoke(_decorated, methodCall.InArgs);
    if (!methodInfo.Name.StartsWith("Get"))
      Log("In Dynamic Proxy - After executing '{0}' ",
        methodCall.MethodName);
      return new ReturnMessage(result, null, 0,
       methodCall.LogicalCallContext, methodCall);
  }
  catch (Exception e)
  {
    if (!methodInfo.Name.StartsWith("Get"))
      Log(string.Format(
        "In Dynamic Proxy- Exception {0} executing '{1}'", e),
        methodCall.MethodName);
      return new ReturnMessage(e, methodCall);
  }
}

プログラムでは、メソッド名が "Get" で始まるかどうかをチェックし、"Get" から始まるメソッドにはアスペクトを適用しません。これはうまくいきますが、フィルターのコードを 3 回繰り返します。また、フィルターがプロキシ内にあるため、プロキシを変更するたびにクラスを変更することになります。これを改善するには、IsValidMethod 述語を作成します。

private static bool IsValidMethod(MethodInfo methodInfo)
{
  return !methodInfo.Name.StartsWith("Get");
}

これで変更は 1 か所だけですむようになりましたが、まだ、フィルターを変更するたびにクラスを変更する必要があります。1 つ解決策は、フィルターをクラス プロパティとして公開し、フィルターを作成する役割を呼び出し元に任せる方法です。Predicate<MethodInfo> 型のフィルター プロパティを作成し、データのフィルター選択に使用します (図 15 参照)。

図 15 フィルター プロキシ

class DynamicProxy<T> : RealProxy
{
  private readonly T _decorated;
  private Predicate<MethodInfo> _filter;
  public DynamicProxy(T decorated)
    : base(typeof(T))
  {
    _decorated = decorated;
    _filter = m => true;
  }
  public Predicate<MethodInfo> Filter
  {
    get { return _filter; }
    set
    {
      if (value == null)
        _filter = m => true;
      else
        _filter = value;
    }
  }
  private void Log(string msg, object arg = null)
  {
    Console.ForegroundColor = ConsoleColor.Red;
    Console.WriteLine(msg, arg);
    Console.ResetColor();
  }
  public override IMessage Invoke(IMessage msg)
  {
    var methodCall = msg as IMethodCallMessage;
    var methodInfo = methodCall.MethodBase as MethodInfo;
    if (_filter(methodInfo))
      Log("In Dynamic Proxy - Before executing '{0}'",
        methodCall.MethodName);
    try
    {
      var result = methodInfo.Invoke(_decorated, methodCall.InArgs);
      if (_filter(methodInfo))
        Log("In Dynamic Proxy - After executing '{0}' ",
          methodCall.MethodName);
        return new ReturnMessage(result, null, 0,
          methodCall.LogicalCallContext, methodCall);
    }
    catch (Exception e)
    {
      if (_filter(methodInfo))
        Log(string.Format(
          "In Dynamic Proxy- Exception {0} executing '{1}'", e),
          methodCall.MethodName);
      return new ReturnMessage(e, methodCall);
    }
  }
}

フィルター プロパティは "_filter = m => true" で初期化しています。つまり、最初はフィルターを無効にします。フィルター プロパティを割り当てるときに、プログラムで値が null かどうかを検証し、null ならフィルターを再設定します。Invoke メソッドの実行時には、プログラムでフィルターの結果をチェックし、true ならばアスペクトを適用します。この時点で、ファクトリ クラスでのプロキシの作成は次のようになります。

public class RepositoryFactory
{
  public static IRepository<T> Create<T>()
  {
    var repository = new Repository<T>();
    var dynamicProxy = new DynamicProxy<IRepository<T>>(repository)
    {
      Filter = m => !m.Name.StartsWith("Get")
      };
      return dynamicProxy.GetTransparentProxy() as IRepository<T>;
    }  
  }
}

フィルターを作成する役割をファクトリに移しています。プログラムを実行すると、図 16 のように表示されます。

Output with a Filtered Proxy
図 16 フィルター プロキシの出力

図 16 の最後の 2 つのメソッド GetAll と GetById ("Getting entities" と "Getting entity 1" で表わしています) ではログ記録が行われません。アスペクトをイベントとして公開することで、このクラスをさらに強化できます。この場合、アスペクトを変更するたびにクラスを変更する必要がありません。これを図 17 に示します。

図 17 柔軟なプロキシ

class DynamicProxy<T> : RealProxy
{
  private readonly T _decorated;
  private Predicate<MethodInfo> _filter;
  public event EventHandler<IMethodCallMessage> BeforeExecute;
  public event EventHandler<IMethodCallMessage> AfterExecute;
  public event EventHandler<IMethodCallMessage> ErrorExecuting;
  public DynamicProxy(T decorated)
    : base(typeof(T))
  {
    _decorated = decorated;
    Filter = m => true;
  }
  public Predicate<MethodInfo> Filter
  {
    get { return _filter; }
    set
    {
      if (value == null)
        _filter = m => true;
      else
        _filter = value;
    }
  }
  private void OnBeforeExecute(IMethodCallMessage methodCall)
  {
    if (BeforeExecute != null)
    {
      var methodInfo = methodCall.MethodBase as MethodInfo;
      if (_filter(methodInfo))
        BeforeExecute(this, methodCall);
    }
  }
  private void OnAfterExecute(IMethodCallMessage methodCall)
  {
    if (AfterExecute != null)
    {
      var methodInfo = methodCall.MethodBase as MethodInfo;
      if (_filter(methodInfo))
        AfterExecute(this, methodCall);
    }
  }
  private void OnErrorExecuting(IMethodCallMessage methodCall)
  {
    if (ErrorExecuting != null)
    {
      var methodInfo = methodCall.MethodBase as MethodInfo;
      if (_filter(methodInfo))
        ErrorExecuting(this, methodCall);
    }
  }
  public override IMessage Invoke(IMessage msg)
  {
    var methodCall = msg as IMethodCallMessage;
    var methodInfo = methodCall.MethodBase as MethodInfo;
    OnBeforeExecute(methodCall);
    try
    {
      var result = methodInfo.Invoke(_decorated, methodCall.InArgs);
      OnAfterExecute(methodCall);
      return new ReturnMessage(
        result, null, 0, methodCall.LogicalCallContext, methodCall);
    }
    catch (Exception e)
    {
      OnErrorExecuting(methodCall);
      return new ReturnMessage(e, methodCall);
    }
  }
}

図 17 では、BeforeExecute、AfterExecute、および ErrorExecuting という 3 つのイベントを、OnBeforeExecute、OnAfterExecute、および OnErrorExecuting の各メソッドで呼び出しています。これらのメソッドでは、イベント ハンドラーが定義されているかを検証し、定義されている場合、呼び出されるメソッドにフィルターが渡されているかどうかをチェックします。渡されている場合、アスペクトを適用するイベント ハンドラーを呼び出します。このファクトリ クラスは図 18 のようになります。

図 18 アスペクト イベントとフィルターを設定するリポジトリ ファクトリ

public class RepositoryFactory
{
  private static void Log(string msg, object arg = null)
  {
    Console.ForegroundColor = ConsoleColor.Red;
    Console.WriteLine(msg, arg);
    Console.ResetColor();
  }
  public static IRepository<T> Create<T>()
  {
    var repository = new Repository<T>();
    var dynamicProxy = new DynamicProxy<IRepository<T>>(repository);
    dynamicProxy.BeforeExecute += (s, e) => Log(
      "Before executing '{0}'", e.MethodName);
    dynamicProxy.AfterExecute += (s, e) => Log(
      "After executing '{0}'", e.MethodName);
    dynamicProxy.ErrorExecuting += (s, e) => Log(
      "Error executing '{0}'", e.MethodName);
    dynamicProxy.Filter = m => !m.Name.StartsWith("Get");
    return dynamicProxy.GetTransparentProxy() as IRepository<T>;
  }
}

柔軟なプロキシ クラスを用意できたので、実行前、実行後、またはエラー発生時に選択したメソッドだけに適用するアスペクトを選択できます。この場合、コードで繰り返しを行わずに、多くのアスペクトを簡単に適用できます。

代わりにはならない

AOP により、コードをアプリケーションのすべての層に追加して一元管理でき、同じコードを繰り返す必要がなくなります。今回は、Decorator デザイン パターンに基づく汎用の動的プロキシを作成し、目的の関数をフィルター選択するためにイベントと述語を使用して、クラスにアスペクトを適用する方法を示しました。

RealProxy クラスは柔軟なクラスで、コードを完全に管理できるようにし、外部との依存関係をなくします。ただし、RealProxy は PostSharp のような他の AOP ツールに代わるものではありません。PostSharp は、まったく異なる手法を使用します。PostSharpではポストコンパイルの手順で中間言語 (IL) コードを追加し、リフレクションは使用しないため、RealProxy よりもパフォーマンスが優れています。また、RealProxy によるアスペクトの実装は PostSharp よりも作業量が多くなります。PostSharp の場合、必要なのは、アスペクト クラスを作成し、アスペクトを追加するクラス (またはメソッド) に属性を追加するだけです。

一方 RealProxy の場合、ソース コードを完全に制御でき、外部との依存関係をなくして、必要なだけ拡張およびカスタマイズできます。たとえば、Log 属性を持つメソッドだけにアスペクトを適用する場合、次のようにします。

 

public override IMessage Invoke(IMessage msg)
{
  var methodCall = msg as IMethodCallMessage;
  var methodInfo = methodCall.MethodBase as MethodInfo;
  if (!methodInfo.CustomAttributes
    .Any(a => a.AttributeType == typeof (LogAttribute)))
  {
    var result = methodInfo.Invoke(_decorated, methodCall.InArgs);
    return new ReturnMessage(result, null, 0,
      methodCall.LogicalCallContext, methodCall);
  }
    ...

 

加えて、RealProxy で使用されるテクニック (コードのインターセプトやプログラムによる置換の許可) は強力です。たとえば、テスト用に汎用のモックやスタブを作成するためにモック フレームワークを作成する場合、RealProxy クラスを使用してすべての呼び出しをインターセプトし、独自の動作に置き換えることができます。ただしこれは今回のテーマではありません。

Bruno Sonnino は、ブラジル在住の Microsoft Most Valuable Professional (MVP) です。彼は開発者、コンサルタント兼執筆者で、5 冊の Delphi の本 (Pearson Education Brazil からポルトガル語で出版) と、ブラジルやアメリカの雑誌、Web サイトでたくさんの記事を執筆しています。

この記事のレビューに協力してくれた、Microsoft Research 技術スタッフの James McCaffrey、Carlos Suarez、および Johan Verwey に心より感謝いたします。
James McCaffrey はワシントン州レドモンドにある Microsoft Research に勤務しています。彼は、Internet Explorer や Bing など、複数のマイクロソフト製品に携わってきました。連絡先は jammc@microsoft.com (英語のみ) です。

Carlos Garcia Jurado Suarez は、Microsoft Research のリサーチ ソフトウェア エンジニアで、高度開発チームや (つい最近では) 機械学習グループで働いてきました。その前は、Visual Studio の開発者として、クラス デザイナーなどのモデリング ツールに携わっていました。