透過プロキシでアスペクト指向プログラミング (1) では、ログ出力やデータベース トランザクションなどの横断的関心事を、
public class NorthwindBusiness : MarshalByRefObject
{
[TraceLog]
[TransactionScope]
public short SelectUnitsInStock()
{
}
}
のように、属性として記述することができました。
今回は属性を使わずに、LINQ と同様にメソッドチェーンを使って横断的関心事を記述してみます。
まず、戻り値を持つ (Func<TResult>
に相当する) 処理を IProxyable<TResult>
インターフェイスとして定義して、
後ろにメソッドチェーンを追加することで処理を上書きできるようにします。
using System; | |
namespace ProxyableConsole | |
{ | |
public interface IProxyable<TResult> | |
{ | |
TResult Execute(); | |
} | |
class BodyProxyable<TResult> : IProxyable<TResult> | |
{ | |
Func<TResult> execute; | |
public BodyProxyable(Func<TResult> execute) | |
{ | |
this.execute = execute ?? throw new ArgumentNullException(nameof(execute)); | |
} | |
public TResult Execute() => execute(); | |
} | |
class AspectProxyable<TResult> : IProxyable<TResult> | |
{ | |
IProxyable<TResult> source; | |
Func<Func<TResult>, TResult> execute; | |
public AspectProxyable(IProxyable<TResult> source, Func<Func<TResult>, TResult> execute) | |
{ | |
this.source = source ?? throw new ArgumentNullException(nameof(source)); | |
this.execute = execute ?? throw new ArgumentNullException(nameof(execute)); | |
} | |
public TResult Execute() => execute(source.Execute); | |
} | |
public static class Proxyable | |
{ | |
public static IProxyable<TResult> Body<TResult>(Func<TResult> execute) => new BodyProxyable<TResult>(execute); | |
public static IProxyable<TResult> Aspect<TResult>(this IProxyable<TResult> source, Func<Func<TResult>, TResult> execute) => new AspectProxyable<TResult>(source, execute); | |
} | |
} |
using System; | |
namespace ProxyableConsole | |
{ | |
class Program | |
{ | |
static void Main(string[] args) | |
{ | |
var result = Proxyable.Body(() => | |
{ | |
Console.WriteLine("Body"); | |
return 123; | |
}) | |
.Aspect(f => | |
{ | |
Console.WriteLine("Before"); | |
var r = f(); | |
Console.WriteLine("After"); | |
return r; | |
}) | |
.Execute(); | |
} | |
} | |
} |
これを実行すると出力は次のようになり、元の処理の前後に処理が追加されていることがわかります。
Before
Body
After
次に、戻り値を持たない (Action
に相当する) 処理を表す IProxyable
インターフェイスを、 IProxyable<TResult>
インターフェイスの特別な場合とみなして継承させます。
using System; | |
namespace ProxyableConsole | |
{ | |
public interface IProxyable : IProxyable<object> | |
{ | |
new void Execute(); | |
} | |
class BodyProxyable : BodyProxyable<object>, IProxyable | |
{ | |
public BodyProxyable(Action execute) : base(() => | |
{ | |
execute?.Invoke(); | |
return null; | |
}) | |
{ | |
} | |
void IProxyable.Execute() => Execute(); | |
} | |
class AspectProxyable : AspectProxyable<object>, IProxyable | |
{ | |
public AspectProxyable(IProxyable source, Action<Action> execute) : base(source, f => | |
{ | |
execute?.Invoke(() => f()); | |
return null; | |
}) | |
{ | |
} | |
void IProxyable.Execute() => Execute(); | |
} | |
public static class Proxyable | |
{ | |
public static IProxyable<TResult> Body<TResult>(Func<TResult> execute) => new BodyProxyable<TResult>(execute); | |
public static IProxyable Body(Action execute) => new BodyProxyable(execute); | |
public static IProxyable<TResult> Aspect<TResult>(this IProxyable<TResult> source, Func<Func<TResult>, TResult> execute) => new AspectProxyable<TResult>(source, execute); | |
public static IProxyable Aspect(this IProxyable source, Action<Action> execute) => new AspectProxyable(source, execute); | |
} | |
} |
using System; | |
namespace ProxyableConsole | |
{ | |
class Program | |
{ | |
static void Main(string[] args) | |
{ | |
Proxyable.Body(() => Console.WriteLine("Body")) | |
.Aspect(a => | |
{ | |
Console.WriteLine("Before"); | |
a(); | |
Console.WriteLine("After"); | |
}) | |
.Execute(); | |
} | |
} | |
} |
あとは、ログ出力やデータベース トランザクションなどの横断的関心事を拡張メソッドとして作成すれば完成です。
using System; | |
using System.Transactions; | |
namespace ProxyableConsole | |
{ | |
class Program | |
{ | |
static void Main(string[] args) | |
{ | |
var units = NorthwindBusiness.SelectUnitsInStock(); | |
NorthwindBusiness.InsertCategory("Books"); | |
try | |
{ | |
NorthwindBusiness.ErrorTest(); | |
} | |
catch (Exception) | |
{ | |
} | |
} | |
} | |
public static class NorthwindBusiness | |
{ | |
public static short SelectUnitsInStock() => | |
Proxyable.Body(() => | |
{ | |
Console.WriteLine("SelectUnitsInStock"); | |
// cf. https://sakapon.wordpress.com/2011/10/02/dirtyread2/ | |
using (var context = new NorthwindEntities()) | |
{ | |
var product = context.Products.Single(p => p.ProductID == 1); | |
return product.UnitsInStock; | |
} | |
}) | |
.TransactionScope() | |
.TraceLog() | |
.Execute(); | |
public static void InsertCategory(string name) => | |
Proxyable.Body(() => | |
{ | |
Console.WriteLine("InsertCategory"); | |
// cf. https://sakapon.wordpress.com/2011/12/14/phantomread2/ | |
using (var context = new NorthwindEntities()) | |
{ | |
context.AddToCategories(Category.CreateCategory(0, name)); | |
context.SaveChanges(); | |
} | |
}) | |
.TransactionScope(IsolationLevel.Serializable) | |
.TraceLog() | |
.Execute(); | |
public static void ErrorTest() => | |
Proxyable.Body(() => throw new InvalidOperationException("This is an error test.")) | |
.TraceLog() | |
.Execute(); | |
} | |
} |
using System; | |
using System.Runtime.CompilerServices; | |
using System.Transactions; | |
namespace ProxyableConsole | |
{ | |
public static class ProxyableExtension | |
{ | |
public static IProxyable<TResult> TraceLog<TResult>(this IProxyable<TResult> source, [CallerMemberName]string caller = "") => | |
source.Aspect(f => | |
{ | |
try | |
{ | |
Console.WriteLine($"Begin: {caller}"); | |
var result = f(); | |
Console.WriteLine($"Success: {caller}"); | |
return result; | |
} | |
catch (Exception ex) | |
{ | |
Console.WriteLine($"Error: {caller}"); | |
Console.WriteLine(ex); | |
throw; | |
} | |
}); | |
public static IProxyable<TResult> TransactionScope<TResult>(this IProxyable<TResult> source, | |
IsolationLevel isolationLevel = IsolationLevel.ReadCommitted, | |
double timeoutInSeconds = 30, | |
TransactionScopeOption scopeOption = TransactionScopeOption.Required) | |
{ | |
var transactionOptions = new TransactionOptions | |
{ | |
IsolationLevel = isolationLevel, | |
Timeout = TimeSpan.FromSeconds(timeoutInSeconds), | |
}; | |
return source.Aspect(f => | |
{ | |
using (var scope = new TransactionScope(scopeOption, transactionOptions)) | |
{ | |
var result = f(); | |
scope.Complete(); | |
return result; | |
} | |
}); | |
} | |
} | |
} |
実行結果です:
使い道としては、
- .NET で透過プロキシを使いたくないとき (処理速度を上げたい、など)
- .NET の属性のような仕組みを持たない別のプラットフォーム
などの場合が考えられるでしょう。
作成したサンプル
ProxyableConsole (GitHub)
バージョン情報
C# 7.0
.NET Framework 4.5