IOCコンテナーのファン向けの合理的なAOP

定型文が嫌いです。 そのようなコードは、うんざりするほど保守的に変更するのは退屈です。 同じ定型文がアプリケーションのビジネスロジックと混在しているときは、まったく気に入らない。 問題は5年前に krestjaninoffによって説明されましたAOPパラダイムに慣れていない場合は、こちらの資料をお読みくださいトピックをカバーしています



この記事を読んだ時点では、 PostSharpもSpringも私に満足していません。 しかし、長年にわたって、ビジネスロジックから「左」コードを抽出し、再利用可能なモジュールを分離して宣言的に記述することを可能にする他のツールが.NETに登場しました。



Castle.DynamicProxyプロジェクトと、エンタープライズアプリケーションの開発におけるそのアプリケーションについて説明します。 私はkrestjaninoffから例を借ります 。なぜなら、うらやましいほどの規則性を備えた同様のコードを見ているからです。

public BookDTO getBook(Integer bookId) throws ServiceException, AuthException { if (!SecurityContext.getUser().hasRight("GetBook")) throw new AuthException("Permission Denied"); LOG.debug("Call method getBook with id " + bookId); BookDTO book = null; String cacheKey = "getBook:" + bookId; try { if (cache.contains(cacheKey)) { book = (BookDTO) cache.get(cacheKey); } else { book = bookDAO.readBook(bookId); cache.put(cacheKey, book); } } catch(SQLException e) { throw new ServiceException(e); } LOG.debug("Book info is: " + book.toString()); return book; }
      
      







したがって、上記の例では、1つの「有用な」操作は、Idによってデータベースから本を読み取ることです。 ロード時に、メソッドは以下を受け取りました。



公平には、承認とアクセス許可、キャッシュは[Authorize ]属性[OutputCache]属性を使用してASP.NETによって提供できることに注意する価値がありますが、この条件は「球状のWebサービス」(Javaでも記述されています) )したがって、ASP.NET、WCF、または企業フレームワークが使用されているかどうかは不明であるため、そのための要件は不明です。



挑戦する





AOPの世界では、私たちが解決しようとしている問題に特別な用語があります: 横断的関心事です。 基本的な懸念事項 -システムの基本機能、たとえばビジネスロジックや分野横断的な懸念事項 -セカンダリ機能(ログ記録、アクセス制御、エラー処理など)は、アプリケーションコードのどこでも必要です。



ほとんどの場合、私は会い、この種の横断的関心事の状況を完全に説明します。

 dbContext.InTransaction(x => { //... }, onFailure: e => {success: false, message: e.Message});
      
      





コードのネストの増加からシステムデザイナーの機能のアプリケーションプログラマーへの移行に至るまで、すべてがい:トランザクションが必要な場所で呼び出されるという保証はなく、トランザクションの分離レベルとネストされたトランザクションのレベルを管理する方法は明確ではなく、このコードは10万回コピーされます必要な場合と必要でない場合。



解決策



Castle.DynamicProxyは、不足しているものをオーバーライドする機能を備えたプロキシオブジェクトをその場で作成するためのシンプルなAPIを提供します。 このアプローチは、一般的な分離フレームワークであるMoqおよびRhino Mocksで使用されています。 次の2つのオプションを使用できます。

  1. インターフェイスリンクを介したプロキシの作成(この場合、構成が使用されます)
  2. クラスのプロキシの作成(相続人が作成されます)


私たちにとっての主な違いは、クラスメソッドを変更するために、アクセス可能( パブリックまたは保護 )および仮想として宣言する必要があることです。 このメカニズムは、 NhibernateまたはEFの 遅延読み込みに似ています。 Castle.DynamicProxyはインターセプターを使用して機能を強化します。 たとえば、すべてのアプリケーションサービスがトランザクション対応であることを確認するには、次のようなインターセプターを記述できます。

  public class TransactionScoper : IInterceptor { public void Intercept(IInvocation invocation) { using (var tr = new TransactionScope()) { invocation.Proceed(); tr.Complete(); } } }
      
      





そして、プロキシを作成します。

 var generator = new ProxyGenerator(); var foo = new Foo(); var fooInterfaceProxyWithCallLogerInterceptor = generator.CreateInterfaceProxyWithTarget(foo, TransactionScoper);
      
      





またはコンテナを使用して

 var builder = new ContainerBuilder(); builder.Register(c => new TransactionScoper()); builder.RegisterType<Foo>() .As<IFoo>() .InterceptedBy(typeof(TransactionScoper)); var container = builder.Build(); var willBeIntercepted = container.Resolve<IFoo>();
      
      





同様に、エラー処理を追加できます。

  public class ErrorHandler : IInterceptor { public readonly TextWriter Output; public ErrorHandler(TextWriter output) { Output = output; } public void Intercept(IInvocation invocation) { try { Output.WriteLine($"Method {0} enters in try/catch block", invoca-tion.Method.Name); invocation.Proceed(); Output.WriteLine("End of try/catch block"); } catch (Exception ex) { Output.WriteLine("Exception: " + ex.Message); throw new ValidationException("Sorry, Unhandaled exception occured", ex); } } } public class ValidationException : Exception { public ValidationException(string message, Exception innerException) :base(message, innerException) { } }
      
      





またはロギング:

  public class CallLogger : IInterceptor { public readonly TextWriter Output; public CallLogger(TextWriter output) { Output = output; } public void Intercept(IInvocation invocation) { Output.WriteLine("Calling method {0} with parameters {1}.", invocation.Method.Name, string.Join(", ", invocation.Arguments.Select(a => (a ?? "").ToString()).ToArray())); invocation.Proceed(); Output.WriteLine("Done: result was {0}.", invocation.ReturnValue); } }
      
      





キャッシングおよびその他の多くの操作。 OOPツールによる「装飾」パターンの実装からのこのアプローチの特徴は、相続人を作成することなく、任意のタイプに補助機能を追加できることです。 このアプローチは、多重継承の問題も解決します。 各タイプに複数のインターセプターを安全に追加できます。

 var fooInterfaceProxyWith2Interceptors = generator.CreateInterfaceProxyWithTarget(Foo, CallLogger, ErrorHandler);
      
      





このアプローチのもう1つの長所は、ビジネスロジック層からエンドツーエンド機能を分離し、インフラストラクチャコードをアプリケーションドメインから最適に分離することです。



登録プロセス中にプロキシする必要があるサービスとそうでないサービスを正確に言うことができない場合、属性を使用して実行時に情報を取得できます(ただし、このアプローチはいくつかの問題につながる可能性があります)。

  public abstract class AttributeBased<T> : IInterceptor where T:Attribute { public void Intercept(IInvocation invocation) { var attrs = invocation.Method .GetCustomAttributes(typeof(T), true) .Cast<T>() .ToArray(); if (!attrs.Any()) { invocation.Proceed(); } else { Intercept(invocation, attrs); } } protected abstract void Intercept(IInvocation invocation, params T[] attr); }
      
      





既製のソリューションを使用することもできます



短所



このアプローチには4つの客観的な欠点があります。

  1. 直感的ではない
  2. 他のフレームワークのインフラストラクチャコードとの交差点
  3. IOCコンテナーの依存関係
  4. 性能




直感的ではない



このようなコードの構造化に対処する最も簡単な方法は、関数型プログラミングの概念に精通している人向けです。 大量の予約があるため、このアプローチは「 構成 」を連想させるものと言えます。 不正に設計されたインターセプターは、明らかではないかなりの量のバグとパフォーマンスの問題を引き起こす可能性があります。



他のフレームワークのインフラストラクチャコードとの交差点



最初に言ったように、 AuthorizeおよびOutputCache属性は既にASP.NETにあります。 ある意味で、私たちは自転車の建設に従事しています。 このアプローチは、最終的な実行インフラストラクチャからの抽象化が重要なチームにより適しています。 さらに、このアプローチは、「すべてまたは何も」ではなく、部分的なアプリケーションのコンテキストで機能します。 AOPスタイルで承認チェックを再実装する必要はありません(これが必要でない場合)。



IOCコンテナーの依存関係



サービスレイヤーでは、IOC / DIを実践している場合、マイナスは実質的にありません。 99%の場合、IOCコンテナを使用してサービスが提供されます。 エンティティとDtoは通常、new演算子またはマッパーを使用して明示的に作成されます。 これは正しい状態だと思いますが、エンティティまたはDtoの作成レベルでインターセプターを使用することはありません。 Entityのサービスフィールドを埋めるためにインターセプターを使用するいくつかの例を見てきましたが、時間の経過とともにこのアプローチは常に放棄されてきました。 オブジェクト自体がその不変式の安全性を処理する方がはるかに優れています。



性能



実用的な理由ではなく正確さのために、前の3つの段落を引用しました。 私はむしろ、実際の問題ではなく、アプローチの適用可能性の限界に起因すると考えています。 パフォーマンスに関しては、確信が持てなかったため、 BenchmarkDotNetを使用して一連のベンチマークを作成することにしました。 ファンタジーにはあまり興味がなかったので、乱数を得るのにかかった時間を測定しました。

  public class Foo : IFoo { private static readonly Random Rnd = new Random(); public double GetRandomNumber() => Rnd.Next(); } public class Foo : IFoo { private static readonly Random Rnd = new Random(); public double GetRandomNumber() => Rnd.Next(); }
      
      





ベンチマークソースとコードサンプルはgithubで入手できます 。 明らかに、リフレクションと動的コンパイルを伴う魔法には代償が伴います:

  1. サイト作成時間:〜2,000 ns。 サービスが一度だけ作成されるかどうかは関係ありませんが、データベースコンテキストなどの「腐敗」依存関係の存続期間に別のオブジェクトが応答します
  2. 実行時間:また、Castle.DynamicProxy Reflection内で約1,000ナノ秒余りが使用され、その後の結果がすべて発生します。


絶対的に言えば、これは非常に多くのことですが、たとえば、データベース内のレコードやネットワーク経由のクエリが発生するなど、コードが50 ns以上実行された場合、状況は異なります。

 public class Bus : Bar { public override double GetRandomNumber() { Thread.Sleep(100); return base.GetRandomNumber(); } }
      
      





ホストプロセス環境情報:
 BenchmarkDotNet = v0.9.8.0
 OS = Microsoft Windows NT 6.2.9200.0
プロセッサー= Intel(R)Core(TM)i7-4710HQ CPU 2.50GHz、ProcessorCount = 8
周波数= 2435775ティック、解像度= 410.5470 ns、タイマー= TSC
 CLR = MS.NET 4.0.30319.42000、Arch = 64ビットリリース[RyuJIT]
 GC =同時ワークステーション
 JitModules = clrjit-v4.6.1080.0


 タイプ= InterceptorBenchmarksモード=スループットGarbageCollection =同時ワークステーション  
 LaunchCount = 1 WarmupCount = 3 TargetCount = 3  
方法 中央値 Stddev
インスタンスを作成 0.0000 ns 0.0000 ns
CreateClassProxy 1,972.0032 ns 8.5611 ns
CreateClassProxyWithTarget 2,246.4208 ns 5.3436 ns
CreateInterfaceProxyWithTarget 2,063.6905 ns 41.9450 ns
CreateInterfaceProxyWithoutTarget 2,105.9238 ns 4.9295 ns
Foo_GetRandomNumber 11.0409 ns 0.1306 ns
Foo_InterfaceProxyGetRandomNumber 51.6061 ns 0.2764 ns
FooClassProxy_GetRandomNumber 9.0125 ns 0.1766 ns
BarClassProxy_GetRandomNumber 44.8110 ns 0.4770 ns
FooInterfaceProxyWithCallLoggerInterceptor_GetRandomNumber 1,756.8129 ns 75.4694 ns
BarClassProxyWithCallLoggerInterceptor_GetRandomNumber 1,714.5871 ns 25.2403 ns
FooInterfaceProxyWith2Interceptors_GetRandomNumber 2,636.1626 ns 20.0195 ns
BarClassProxyWith2Interceptors_GetRandomNumber 2,603.6707 ns 4.6360 ns
Bus_GetRandomNumber 100,471,410.5375 ns 113,713.1684 ns
BusInterfaceProxyWith2Interceptors_GetRandomNumber 100,539,356.0575 ns 89,725.5474 ns
CallLogger_Intercept 3,841.4488 ns 26.3829 ns
ライトライン 859.0076 ns 34.1630 ns
ReflectionをキャッシュされたLambdaExpressionに置き換えると、パフォーマンスにまったく違いはないことを実現できる思いますが、そのためにDynamicProxyを書き換え、一般的なコンテナーにサポートを追加する必要があります(現在、インターセプターはAutofacおよびCastle.Windsorボックスから正確にサポートされていますが 、他のユーザーについてはわかりません) 。 私はこれが近い将来起こるとは思わない。



したがって、平均で少なくとも100ミリ秒間操作が実行され、前の3つのマイナスが怖がらない場合、C#の「コンテナAOP」はすでに生産準備完了です。



All Articles