アスペクト指向プログラミングって何?
古今東西、さまざまな○○指向が存在していますが、最近アスペクト指向プログラミング(AOP)という用語が存在していることを知りました。
調べてみたところ、JavaのSpring Frameworkでは一般的に使われてそうな概念のようです。
ということで、AOPについて調べてみました。
アスペクト指向プログラミング(AOP)とは
AOPとは、横断的な関心事(=複数の場所から利用される共通処理)をビジネスロジックから分離することで、ソフトウェアのモジュール性を高めるためのプログラミングパラダイムです。
例えば、以下のようなコードを書く場面があるのではないでしょうか。
class TestClass
{
public void MethodA(string filePath)
{
// filePathに対する検証処理
// 本体のロジック
// ...
// ログ出力
}
public void MethodB(string filePath)
{
// filePathに対する検証処理
// 本体のロジック
// ...
// ログ出力
}
}
上記のコードでは、入力値に対する検証処理やログ出力などのコードが複数のメソッド間で共通に書かれています。
AOPではこのような共通のコードを分離して、本来やりたい処理だけに注目して書けるようにする、というのが目的のようです。
C#での実装
調べてみたところ、C#で実装する場合、DispatchProxyを利用したやり方が多くヒットしたのですが、今回はより柔軟な実装ができそうな属性を利用するパターンで、入力のバリデーションを行うサンプルプログラムを書いてみたいと思います。
なお、今回は.NET 6を前提とします。
まず、下準備としてCastle.CoreのNuGetパッケージをインストールします。
次に、以下のような属性クラスを作ります。
[AttributeUsage(AttributeTargets.Method)]
public class ValidateStringAttribute : Attribute
{
/// <summary>
/// バリデーション対象の引数
/// </summary>
public string TargetArgument { get; private set; }
/// <summary>
/// バリデーション方法
/// </summary>
public string ValidationRule { get; private set; }
/// <summary>
/// コンストラクタ
/// </summary>
public ValidateStringAttribute(string targetArgument, string validationRule)
{
TargetArgument = targetArgument;
ValidationRule = validationRule;
}
}
ValidateString属性では、変数名とその変数に応じた検証方法を指定できるようにしてみました。
この属性を共通処理を実行したいメソッドに付与することで検証が実行されるイメージです。
属性の有無によって検証処理を実行するInterceptorクラスも作成します。
internal class ValidationInterceptor : IInterceptor
{
/// <summary>
/// 検証処理を挿入する
/// </summary>
public void Intercept(IInvocation invocation)
{
// インターセプトされたメソッドにValidateString属性が存在するか
var validateStringAttribute = invocation
.MethodInvocationTarget
.GetCustomAttributes(typeof(ValidateStringAttribute), false)
.FirstOrDefault() as ValidateStringAttribute;
// ValidateString属性が存在する場合、検証処理を実行する。
if (validateStringAttribute is not null)
{
var methodInfo = invocation.Method;
var targetParameter = methodInfo.GetParameters()
.Select((info, index) => new {Info = info, Index = index})
.FirstOrDefault(tupple => tupple.Info.Name == validateStringAttribute.TargetArgument);
if (targetParameter != null && invocation.Arguments[targetParameter.Index] is string argument)
{
if(validateStringAttribute.ValidationRule == "Path")
{
ValidatePath(argument);
}
}
}
// メソッドを実行する
invocation.Proceed();
}
/// <summary>
/// パスを検証する
/// </summary>
private void ValidatePath(string path)
{
Console.WriteLine("パスを検証します。");
if (string.IsNullOrEmpty(path))
{
throw new ArgumentException("不正なパスです。");
}
Console.WriteLine("検証成功。");
}
}
また、インターフェースと、そのインターフェースを実装した属性を利用するクラスを定義します。
public interface ITestClass
{
void MethodA(string filePath);
void MethodB(string filePath);
}
public class TestClass : ITestClass
{
[ValidateString("filePath", "Path")]
public void MethodA(string filePath)
{
Console.WriteLine($"{nameof(MethodA)}:{filePath}");
}
[ValidateString("filePath", "Path")]
public void MethodB(string filePath)
{
Console.WriteLine($"{nameof(MethodB)}:{filePath}");
}
}
これらを利用するコードは以下です。
// proxyでラップする
var proxyGenerator = new ProxyGenerator();
var testClass = proxyGenerator.CreateInterfaceProxyWithTarget<ITestClass>(new TestClass(), new ValidationInterceptor());
// メソッドを実行
testClass.MethodA(@"C:\Git");
testClass.MethodB(@"C:\Git\samples");
// 出力:
// >パスを検証します。
// >検証成功。
// >MethodA:C:\Git
// >パスを検証します。
// >検証成功。
// >MethodB:C:\Git\samples
ProxyGeneratorに属性を付与したメソッドを持つクラスのインスタンスとインターセプターを渡すことで、対象のメソッド実行時にバリデーションを実行できます。
インターセプターは複数渡すことができるので、バリデーションとログ出力の両方を実現するなんてこともできそうです。
まとめ
本記事ではAOPをC#で実現してみました。
リフレクションってなんでもできるんだなぁというのが率直な感想です。
愚直にバリデーションした場合とどの程度パフォーマンスに差が出るのかも気になりますね。
気になる人はぜひ測ってみてください。そして教えてください。
参考文献
Discussion