AOP(Aspect Oriented Programming) の解説
- AOP とは何か
- AOP を利用するメリット・デメリット
- Autofacを利用した例
- その他
AOP とは何か
Wikipediaでは以下のように説明されている。
オブジェクト指向ではうまく分離できない特徴(クラス間を横断 (cross-cutting) するような機能)を「アスペクト」とみなし、アスペクト記述言語をもちいて分離して記述することでプログラムに柔軟性をもたせようとする試み。
例えばRDBでのトランザクション処理を実行する場合、大抵は処理そのものが成功した場合はコミットし、データベース上のデータの条件が整わない場合や処理自体にエラーが存在し例外が発生するような場合にロールバックするし、処理を実行する場合にトランザクションを実行する区間だけが分かればよい。
この時対象の処理の本体に beginTransaction,commitTransactionを記述するよりもメソッドに「このメソッドではトランザクション処理を実行し、成功したらコミット、失敗したらロールバックする」と言う宣言を行うだけで済むように出来るなら、メソッドの本体からはトランザクションに関する知識を持ち込まなくて良い。
これを可能にするのがAOP(Aspect Oriented Programming)である。
例
以下の例はメソッドにTrace属性を付けてメソッド呼び出しトレースを実行する。
public class FooClient
{
private readonly IFooComponent component;
public FooClient(IFooComponent component)
{
this.component = component;
}
public void Execute()
{
// logger.Log("component.Execute") とかやらなくてもトレースが出力される
var result = component.Execute();
Console.WriteLine("foo client execute {0}", result);
}
}
public interface IFooComponent
{
bool Execute();
}
public class FooComponent : IFooComponent
{
[Trace]
public virtual bool Execute()
{
// logger.Log("FooComponent.Execute start") とかやらなくてもトレースが出力される
// このConsoleへの出力は単なるサンプル。現実はもっと有意義な事をやる
Console.WriteLine("foo component execute");
return true;
}
}
///<summary>メソッド実行トレースを設定するだけの属性</summary>
public class TraceAttribute{
}
///<summary>例えばNLogにメソッド実行のトレースを出力するとか</summary>
public class TraceInterceptor : IInterceptor{
public void Intercept(IInvocation invocation)
{
this.invocation = invocation;
this.logger = GetLogger();
logLevel = GetLogLevel();
Log("begin");
invocation.Proceed();
Log("end");
return;
}
}
このようにすることでメソッド本体で本来実行するべき内容に集中することができる。
また、場合によってはメソッドの呼び出し自体を乗っ取ることができるので引数の改変や戻り値の改変、あるいは処理そのものを継承によらずに上書きすることができる。
(大抵の実装では対象クラスを継承したプロキシクラスのオブジェクトを生成したうえで必要なInterceptorを呼び出しているので、厳密には「継承によらない」わけではない)
AOP を利用するメリット・デメリット
メリット
- 処理の本体そのものの記述に集中できる
- 処理の本体を変更せずに動作を追加、変更できる
デメリット
- DIコンテナの構築等の実行時間が増える
- 呼出しのすべてがコードの記述通りではなくなるのでややこしい?
- 学習コストが上がる
呼出しのすべてがコードの記述通りではなくなるのでややこしい?に関して、そうなる故処理そのものの実行に関わるAspectは注意して使う必要がある。
一般的にはログ出力、トランザクション、例外処理など。
Autofacを利用した例
Trace属性を付けたメソッドの開始、終了をTraceに出力する例。
- Aspectを適用するための属性を記述する
- Interceptorを記述する
- コンポーネントクラスを記述する
- クライアントクラスを記述する
- DIコンテナを構成する
- DIコンテナから必要なオブジェクトを取り出す
- 全体のコード
Aspectを適用するための属性を記述する
Aspectの適用対象をマークするための属性を記述する。
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = false, Inherited = true)]
public class TraceAttribute : Attribute
{
public const string Off = "Off";
public const string Debug = "Debug";
public const string Error = "Error";
public const string Fatal = "Fatal";
public const string Info = "Info";
public const string Trace = "Trace";
public const string Warn = "Warn";
public string Level { get; set; } = Trace;
internal LogLevel LogLevel { get => LogLevel.FromString(Level); }
public TraceAttribute()
{
}
}
ここではNLogが利用するログレベルを設定しているが、NLog.LogLevel.Trace などの固定レベルを属性の初期値に利用できない([Trace(LogLevel=NLog.LogLevel.Trace)]とかできない)ので属性用のプロパティとInterceptorが参照するプロパティを分離している。
対象をマークするだけなので処理は書かなくて良い。
Interceptor実装と属性を同じクラスに出来るなら、そうしてよい。(ASP.NET MVCではそのような属性が色々ある)
Interceptorを記述する
Interceptorの本体を実装記述する。
public class TraceInterceptor : IInterceptor
{
private ILogger logger;
private LogLevel logLevel;
private IInvocation invocation;
public TraceInterceptor()
{
}
public void Intercept(IInvocation invocation)
{
this.invocation = invocation;
this.logger = GetLogger();
logLevel = GetLogLevel();
Log("begin");
invocation.Proceed();
Log("end");
return;
}
private ILogger GetLogger()
{
var logger = LogManager.GetLogger(invocation.TargetType.FullName);
return logger;
}
private LogLevel GetLogLevel()
{
var methodAttributes = invocation.Method.GetCustomAttributes(typeof(TraceAttribute), true);
if (methodAttributes.Length > 0)
{
var methodAttribute = methodAttributes[0] as TraceAttribute;
return methodAttribute.LogLevel;
}
return LogLevel.Off;
}
private void Log(string message)
{
if (logLevel != LogLevel.Off)
{
if (logger.IsEnabled(logLevel))
{
logger.Log(logLevel, "#" + invocation.Method.Name + " " + message);
}
}
}
}
Interceptorとして呼び出されるのは Invoke(invocation)となるので、処理を実装する。
コンポーネントクラスを記述する
クライアントにはインターフェースだけを公開する場合。
public interface IFooComponent
{
[Trace]
bool Execute();
}
public class FooComponent : IFooComponent
{
public virtual bool Execute()
{
Console.WriteLine("foo component execute");
return true;
}
}
インターフェースの実装クラスすべてでメソッド「Execute」を呼び出す場合にAspectが適用されるようにする場合、インターフェースの対象メソッドに属性を付ける。特定実装クラスだけで良いなら実装クラスの方に付ける。
重要なのは 実装の対象メソッドを"virtual"にしておくこと。Autofac(Castle.Proxyを含む)を使う場合、コンポーネントは実行時に継承されたオブジェクトが取り出され、Aspect対象をoverrideしているのでこのようにする必要がある。
クライアントクラスを記述する
DIでのサンプルと同じ。
public class FooClient
{
private readonly IFooComponent component;
public FooClient(IFooComponent component)
{
this.component = component;
}
public void Execute()
{
var result = component.Execute();
Console.WriteLine("foo client execute {0}", result);
}
}
DIコンテナを構成する
コンテナに利用するコンポーネントを登録してコンテナを構成する。
var builder = new ContainerBuilder();
builder.RegisterType<TraceInterceptor>();
builder.RegisterType<FooClient>();
builder.RegisterType<FooComponent>()
.As<IFooComponent>()
.EnableInterfaceInterceptors()
.InterceptedBy(typeof(TraceInterceptor));
return builder.Build();
Interceptorをコンポーネントとして登録する事、コンポーネントへのInterceptorの注入が増える。
DIコンテナから必要なオブジェクトを取り出す
DIでのサンプルと同じ。
var client = container.Resolve<FooClient>();
client.Execute();
全体のコード
class Program
{
static void Main(string[] args)
{
LogManager.LoadConfiguration("nlog.config");
var container = BuildContainer();
var client = container.Resolve<FooClient>();
client.Execute();
Console.ReadLine();
}
// 設定ファイルを使う場合
//static IContainer BuildContainer()
//{
// var config = new ConfigurationBuilder();
// config.AddJsonFile("components.json");
// var module = new ConfigurationModule(config.Build());
// var builder = new ContainerBuilder();
// builder.RegisterModule(module);
// return builder.Build();
//}
static IContainer BuildContainer()
{
var builder = new ContainerBuilder();
builder.RegisterType<TraceInterceptor>();
builder.RegisterType<FooClient>();
builder.RegisterType<FooComponent>()
.As<IFooComponent>()
.EnableInterfaceInterceptors()
.InterceptedBy(typeof(TraceInterceptor));
return builder.Build();
}
}
public class DiSampleModule : Module
{
protected override void Load(ContainerBuilder builder)
{
builder.RegisterType<FooClient>();
builder.RegisterType<FooComponent>().As<IFooComponent>();
}
}
public class FooClient
{
private readonly IFooComponent component;
public FooClient(IFooComponent component)
{
this.component = component;
}
public void Execute()
{
var result = component.Execute();
Console.WriteLine("foo client execute {0}", result);
}
}
public interface IFooComponent
{
[Trace]
bool Execute();
}
public class FooComponent : IFooComponent
{
public virtual bool Execute()
{
Console.WriteLine("foo component execute");
return true;
}
}
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = false, Inherited = true)]
public class TraceAttribute : Attribute
{
public const string Off = "Off";
public const string Debug = "Debug";
public const string Error = "Error";
public const string Fatal = "Fatal";
public const string Info = "Info";
public const string Trace = "Trace";
public const string Warn = "Warn";
public string Level { get; set; } = Trace;
internal LogLevel LogLevel { get => LogLevel.FromString(Level); }
public TraceAttribute()
{
}
}
public class TraceInterceptor : IInterceptor
{
private ILogger logger;
private LogLevel logLevel;
private IInvocation invocation;
public TraceInterceptor()
{
}
public void Intercept(IInvocation invocation)
{
this.invocation = invocation;
this.logger = GetLogger();
logLevel = GetLogLevel();
Log("begin");
invocation.Proceed();
Log("end");
return;
}
private ILogger GetLogger()
{
var logger = LogManager.GetLogger(invocation.TargetType.FullName);
return logger;
}
private LogLevel GetLogLevel()
{
var methodAttributes = invocation.Method.GetCustomAttributes(typeof(TraceAttribute), true);
if (methodAttributes.Length > 0)
{
var methodAttribute = methodAttributes[0] as TraceAttribute;
return methodAttribute.LogLevel;
}
return LogLevel.Off;
}
private void Log(string message)
{
if (logLevel != LogLevel.Off)
{
if (logger.IsEnabled(logLevel))
{
logger.Log(logLevel, "#" + invocation.Method.Name + " " + message);
}
}
}
}
見てわかる通り、FooClientの変更はなく、FooComponentもトレース出力処理が増えたことはメソッド本体には影響していない。
その他
- デメリットで上げた通り、やりすぎると処理が混乱する元になる。
- 例えばデータの変更記録をデータベースに出力する、等もできる。
- Interceptor自体の例外処理をうまくやってないと、例外が発生した場合にコードを追うのが困難になる。