ラベル AOP の投稿を表示しています。 すべての投稿を表示
ラベル AOP の投稿を表示しています。 すべての投稿を表示

2012年7月16日月曜日

c#でインスタンスをFreeze

クラスをジェネレートする際にメタデータでFreezableかどうか保持して作るのが楽ちんなのかなー。

Castle.DynamicProxyとPostSharpで実装してみました。

DynamicProxy | Castle Project

どうやってインストールするんですか!と、思ったけど、NuGetですね。Castle.Core で。

The Creators of PostSharp – SharpCrafters

こっちもNuGetで。ただ、無料のBASICライセンスを別途取得する必要がありました。むかーし、登録したアカウントが邪魔してちょっと手間取った...。

Castle.DynamicProxy

まずはCastle。ベケットは出てこないよ。

Dynamic Proxy Tutorial « Krzysztof Koźmic on software

ここに書いてるとおりです!

ようするに動的に派生クラスを生成するFactoryを使ってインスタンスを作るのと、virtualなプロパティ/メソッドに対してインターセプトできるようになる、っていうのがCastle.DynamicProxyです。overrideね。

    public interface IFreezable
    {
        bool IsFrozen { get; }
        void Freeze();
    }

Freezeできるようにするためのインターフェース。CastleでもPostSharpでも同じのを使うよ。

    public class Animal
    {
        public virtual string Name { get; set; }
        public virtual int Age { get; set; }

        public override string ToString()
        {
            return string.Format("Name: {0}, Age: {1}", Name, Age);
        }
    }

こんなモデルクラスを用意します。もちろんvirtualで。だけど、これ自体がIFreezableを実装する必要はないので注意。

ここからは、ちょっと長めのコードになるけど、簡単に説明するとIFreezableを実装したInterceptorを実装し、それを注入したProxyインスタンスを生成。Proxyインスタンスではvirtualなものにアクセスした場合、Interceptorが実行されるのでその時都度Freezeされたかチェックする、っていう流れです。

proxy_pipeline

こんなイメージ。公式サイトより

    class FreezableInterceptor : IInterceptor, IFreezable
    {
        private bool _isFrozen;

        public void Freeze()
        {
            _isFrozen = true;
        }

        public bool IsFrozen
        {
            get { return _isFrozen; }
        }

        public void Intercept(IInvocation invocation)
        {
            if (_isFrozen && invocation.Method.Name.StartsWith("set_", StringComparison.OrdinalIgnoreCase))
            {
                throw new FreezableException("object frozen!");
            }

            invocation.Proceed();
        }
    }

Interceptorが↑これ。続いてヘルパーのコード。

    static class Freezable
    {
        private static readonly ProxyGenerator _generator = new ProxyGenerator();

        public static bool IsFreezable(object obj)
        {
            return AsFreezable(obj) != null;
        }

        private static IFreezable AsFreezable(object target)
        {
            if (target == null)
                return null;
            var accessor = target as IProxyTargetAccessor;
            if (accessor == null)
                return null;
            return accessor.GetInterceptors().OfType<FreezableInterceptor>().FirstOrDefault();
        }

        public static bool IsFrozen(object obj)
        {
            var freezable = AsFreezable(obj);
            return freezable != null && freezable.IsFrozen;
        }

        public static void Freeze(object freezable)
        {
            var interceptor = AsFreezable(freezable);
            if (interceptor == null)
                throw new FreezableException("Not freezalbe!");
            interceptor.Freeze();
        }

        public static TFreezable CreateInstance<TFreezable>() where TFreezable : class, new()
        {
            var freezableInterceptor = new FreezableInterceptor();
            var proxy = _generator.CreateClassProxy<TFreezable>(freezableInterceptor);
            return proxy;
        }
    }

こっちがちょっと長い。ProxyインスタンスはIProxyTargetAccessorを実装してるので、それを利用して、Interceptorを取得してFreezeしたりIsFrozeしたりです。

    [TestClass]
    public class UnitTest1
    {
        [TestMethod]
        public void Freezeさせる()
        {
            var rex = Freezable.CreateInstance<Animal>();
            rex.Name = "Rex";
            Console.WriteLine(Freezable.IsFreezable(rex)
                                  ? "Rex is freezable!"
                                  : "Rex is not freezable. Something is not working");
            Console.WriteLine(rex.ToString());
            Console.WriteLine("Add 50 years");
            rex.Age += 50;
            Console.WriteLine("Age: {0}", rex.Age);

            Freezable.Freeze(rex);
            Console.WriteLine("Frozen? : " + Freezable.IsFrozen(rex));
            try
            {
                rex.Age++;
                Console.WriteLine("あれー?変更できるー。");
            }
            catch
            {
                Console.WriteLine("Frozen!!");
            }
        }

        [TestInitialize]
        public void Initialize()
        {
            var instance1 = new Animal();
            var instance2 = Freezable.CreateInstance<Animal>();            
        }

        [TestMethod]
        public void 普通のインスタンス作る()
        {
            var sw = new Stopwatch();
            sw.Start();
            for (var i = 0; i < 10000;i++ )
            {
                var instance = new Animal();
            }
            sw.Stop();

            Console.WriteLine("create 10000 instances at {0} ms",sw.ElapsedMilliseconds);
        }

        [TestMethod]
        public void Proxyのインスタンス作る()
        {
            var sw = new Stopwatch();
            sw.Start();
            for (var i = 0; i < 10000; i++)
            {
                var instance = Freezable.CreateInstance<Animal>();
                //Freezable.Freeze(instance);
            }
            sw.Stop();

            Console.WriteLine("create 10000 instances at {0} ms", sw.ElapsedMilliseconds);
        }

        [TestMethod]
        public void Proxyのインスタンス作ってFreezeする()
        {
            var sw = new Stopwatch();
            sw.Start();
            for (var i = 0; i < 10000; i++)
            {
                var instance = Freezable.CreateInstance<Animal>();
                Freezable.Freeze(instance);
            }
            sw.Stop();

            Console.WriteLine("create 10000 instances at {0} ms", sw.ElapsedMilliseconds);
        }

        [TestMethod]
        public void 普通のインスタンスでプロパティアクセス()
        {
            var sw = new Stopwatch();
            sw.Start();
            for (var i = 0; i < 10000; i++)
            {
                var instance = new Animal();
                instance.Age = 200;
                instance.Name = "Foo";
            }
            sw.Stop();

            Console.WriteLine("create 10000 instances at {0} ms", sw.ElapsedMilliseconds);
        }

        [TestMethod]
        public void Proxyのインスタンスでプロパティアクセス()
        {
            var sw = new Stopwatch();
            sw.Start();
            for (var i = 0; i < 10000; i++)
            {
                var instance = Freezable.CreateInstance<Animal>();
                instance.Age = 200;
                instance.Name = "Foo";
                //Freezable.Freeze(instance);                
            }
            sw.Stop();

            Console.WriteLine("create 10000 instances at {0} ms", sw.ElapsedMilliseconds);
        }
    }

動かしましょう。

  • Freezeさせる[0:00.218] Success
  • Proxyのインスタンスでプロパティアクセス[0:00.106] Success
  • Proxyのインスタンス作ってFreezeする[0:00.117] Success
  • Proxyのインスタンス作る[0:00.109] Success
  • 普通のインスタンスでプロパティアクセス[0:00.003] Success
  • 普通のインスタンス作る[0:00.002] Success

こんな感じです。Proxy経由は若干遅くなりますね。TestInitializeでわざわざインスタンス作ってるのは、Proxyの生成をここでやらせるためです。こうしとけば、各テストの計測では実行分のみの時間になるから。

DynamicProxyで、うーんと思ったのはvirtualの強制。まぁねー。クラス生成するんだし、そーだよなー、とは思いつつも。

PostSharp

続いてPostSharp。こっちはアセンブリを書き換えてインターセプト。クラス生成よりも強力。ちょっと使い方が分かりにくいところもあるけど、大体なんでもできちゃう系。

今回はInstanceLevelAspectでモデルクラスにインターフェース(IFreezable)を実装し、LocationInterceptionAspectでプロパティアクセスをインターセプト。OnSetValueでIsFrozenを確認してるよ。

    [Serializable]
    [IntroduceInterface(typeof(IFreezable))]
    class FreezableAttribute : InstanceLevelAspect, IFreezable
    {
        [NonSerialized]
        private bool _isFrozen;
        public bool IsFrozen
        {
            get { return _isFrozen; }
        }

        public void Freeze()
        {
            _isFrozen = true;
        }
    }

まずは、既存クラスにインターフェースを後付Mixin

    [Serializable]
    class FrozenAttribute : LocationInterceptionAspect
    {
        public override void OnSetValue(LocationInterceptionArgs args)
        {
            var freezable = args.Instance as IFreezable;
            if (freezable != null && freezable.IsFrozen)
            {
                throw new FreezableException(string.Format("frozen {0}", args.LocationFullName));
            }

            args.ProceedSetValue();
        }
    }

インターフェース実装されてるなら、Freezableだからプロパティアクセスにインターセプト。

これだけ。これだけだけど、これらを1個のAspectにしようとしてスゴイ遠回りした。結果、インターフェースのMixinとインターセプトは同一Aspectにできない、みたい。できる方法があるなら教えて!

    [Freezable, Frozen]
    public class Person
    {
        public string Name { get; set; }
        public int Age { get; set; }

        public override string ToString()
        {
            return string.Format("Name: {0}, Age: {1}", Name, Age);
        }
    }

モデルね。属性に2つ書かないといけないのが残念ポイントだけど、virtualでもないし、これならCollection/List/DictionaryいろいろReadOnlyにしてしまえます。ね。

    [TestClass]
    public class UnitTest2
    {
        [TestMethod]
        public void Freezeさせる()
        {
            var rex = new Person();
            rex.Name = "Rex";
            Console.WriteLine(rex.ToString());
            Console.WriteLine("Add 50 years");
            rex.Age += 50;
            Console.WriteLine("Age: {0}", rex.Age);

            rex.Freeze();
            Console.WriteLine("Frozen? : " + rex.IsFrozen());

            try
            {
                rex.Age++;
                Console.WriteLine("あれー?変更できるー。");
            }
            catch
            {
                Console.WriteLine("Frozen!!");
            }
        }

        [TestMethod]
        public void Newする()
        {
            var sw = new Stopwatch();
            sw.Start();
            for (var i = 0; i < 10000; i++)
            {
                var instance = new Person();
                instance.Freeze();
            }
            sw.Stop();

            Console.WriteLine("create 10000 instances at {0} ms", sw.ElapsedMilliseconds);
        }

        [TestMethod]
        public void NewしてFreezeする()
        {
            var sw = new Stopwatch();
            sw.Start();
            for (var i = 0; i < 10000; i++)
            {
                var instance = new Person();
                instance.Freeze();
            }
            sw.Stop();

            Console.WriteLine("create 10000 instances at {0} ms", sw.ElapsedMilliseconds);
        }

        [TestMethod]
        public void Newしてプロパティアクセス()
        {
            var sw = new Stopwatch();
            sw.Start();
            for (var i = 0; i < 10000; i++)
            {
                var instance = new Person();
                instance.Age = 200;
                instance.Name = "Foo";
            }
            sw.Stop();

            Console.WriteLine("create 10000 instances at {0} ms", sw.ElapsedMilliseconds);
        }
    }

こんなテストで実行。

  • Freezeさせる[0:00.084] Success
  • NewしてFreezeする[0:00.015] Success
  • Newしてプロパティアクセス[0:00.012] Success
  • Newする[0:00.004] Success

素のクラスに比べればそれでも遅くはなるけど、DynamicProxyよりは高速。そりゃそーだ。アセンブリ書き換えてんだから。

1個の属性でできるとうれしいなー。

いずれにせよ、モデルクラス1個ずつFreezableをプロパティごとに実装なんてコード生成以外にありえないと思うじゃない。でも、インターセプトするなら、それも何とか乗り越えられる。その際にProxyクラスにするか、アセンブリ書き換えるかは好みでいいとは思うけど、既存コードほとんどいじらず(Factoryでインスタンス作らなくていい)に済むし早いしでPostSharpが使いやすいかなーと思う海の日。

2010年1月4日月曜日

PostSharp興味深い

PostSharp - Bringing AOP to .NET

AOPフレームワークと言われるんだって。

ボスの大嫌いなRemotingのProxy(.NET Framework 2.0 コア機能解説 ~ 第 1 回 .NET リモーティング ~)でのフックや(A basic proxy for intercepting method calls (Part –1) - Mehfuz's WebLog)、Invokerを必ず呼び出すASP.NET MVCでのFilterAttribute達のどっちかが一般的なのかと思ってたけど、世の中スゴイ事思いつく人たちがいるモンで、ビルド後にILを書き換えてフックポイントを追加してしまえばいいじゃないかという発想で作られてます。まさにEmitにを超える黒魔術。

ドキュメントに書かれてる実装パターン。

int MyMethod(object arg0, int arg1)
{
  OnEntry();
  try
  {
    // Original method body. 

    OnSuccess();
    return returnValue;
  }
  catch ( Exception e )
  {
    OnException();
  }
  finally
  {
    OnExit();
  }
}

Reflectorなんかでビルド後のアセンブリを見ると上記のパターンにそったものが出力されてるのが確認出来ます。

postsharp1 

確かに~、確かに~。

ドキュメントを見つつ、どんなことが出来るのかチラッとコード書いてみたんです。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using PostSharp.Laos;

namespace TestPostSharp
{
  class Program
  {
    [ThirdAspect]
    private static int _intFldValue;

    static void Main(string[] args)
    {
      Console.WriteLine("Start program.");

      PrintMessage("Hello, World! - first");
      PrintMessage("Hello, World! - second");
      PrintMessage("Hello, World! - third");
      
      var res = Invocation(100,"まじか!",DateTime.Now);
      Console.WriteLine("res = {0}",res);

      _intFldValue = 100;
      Console.WriteLine("Get field {0}", _intFldValue);

      Console.WriteLine("End program.");
      Console.ReadKey();
    }

    [FirstAspect]
    static void PrintMessage(string message)
    {
      Console.WriteLine(message);
    }

    [SecondAspect]
    static int Invocation(int p1, string p2, DateTime dateTime)
    {
      Console.WriteLine("何かか実行されるようだ");
      Console.WriteLine("{0} - {1} - {2}",p1,p2,dateTime);

      return p1;
    }
  }

  [Serializable]
  public class FirstAspectAttribute:OnMethodBoundaryAspect
  {
    public override void OnEntry(MethodExecutionEventArgs eventArgs)
    {
      Console.WriteLine(eventArgs.Method.Name + " - OnEntry");
      base.OnEntry(eventArgs);
    }

    public override void OnExit(MethodExecutionEventArgs eventArgs)
    {
      Console.WriteLine(eventArgs.Method.Name + " - OnExit");
      base.OnExit(eventArgs);
    }

    public override void OnSuccess(MethodExecutionEventArgs eventArgs)
    {
      Console.WriteLine(eventArgs.Method.Name + " - OnSuccess");
      base.OnSuccess(eventArgs);
    }

    public override void OnException(MethodExecutionEventArgs eventArgs)
    {
      Console.WriteLine(eventArgs.Method.Name + " - OnException");
      base.OnException(eventArgs);
    }

    public override void RuntimeInitialize(System.Reflection.MethodBase method)
    {
      Console.WriteLine(method.Name + " - RuntimeInitialize");
      base.RuntimeInitialize(method);
    }

    public override void CompileTimeInitialize(System.Reflection.MethodBase method)
    {
      Console.WriteLine(method.Name + " - CompileTimeInitialize");
      base.CompileTimeInitialize(method);
    }

    public override bool CompileTimeValidate(System.Reflection.MethodBase method)
    {
      Console.WriteLine(method.Name + " - CompileTimeValidate");
      return base.CompileTimeValidate(method);
    }
  }

  [Serializable]
  public class SecondAspect : OnMethodInvocationAspect
  {
    public override void OnInvocation(MethodInvocationEventArgs eventArgs)
    {
      Console.WriteLine("Calling {0}", eventArgs.Method.Name);

      var args = eventArgs.GetArgumentArray();
      if (args[0].GetType() == typeof(int))
        args[0] = (int) args[0] * 2;

      eventArgs.Proceed(args);
      var res = eventArgs.ReturnValue;
      eventArgs.ReturnValue = (int) res + 100;
    }
  }

  [Serializable]
  public class ThirdAspect : OnFieldAccessAspect
  {
    public override void OnGetValue(FieldAccessEventArgs eventArgs)
    {
      Console.WriteLine("Getter {0} = {1}", eventArgs.FieldInfo.Name, eventArgs.StoredFieldValue);
      
      base.OnGetValue(eventArgs);
    }

    public override void OnSetValue(FieldAccessEventArgs eventArgs)
    {
      Console.WriteLine("Setter {0} = {1} -> {2}", eventArgs.FieldInfo.Name, eventArgs.StoredFieldValue, eventArgs.ExposedFieldValue);
      var value = eventArgs.ExposedFieldValue;
      eventArgs.ExposedFieldValue = (int) value*2;
      
      base.OnSetValue(eventArgs);
    }
  }
}

postsharp2

最初から用意されてる便利クラス。

  • OnExceptionAspect
  • OnMethodBoundaryAspect
  • OnMethodInvocationAspect
  • OnFieldAccessAspect

それぞれ、だいたい名前の通り。引数や戻り値を書き換えたりも出来るよ!IOn~のインターフェースも用意されてるから、用意されてるのが気に入らないなら最初から実装してしまうのも可能。OnExceptionAspectについてはその他のクラスのOnExceptionでも取れるから単体で使う場面はそんなに無かったりするのかな~。どうなんでしょう。

属性クラスとしてAspectクラスを実装して、クラスやメソッド、フィールドに指定してビルドするのがオーソドックスな使い方だと思うけど、更に[assembly:自作Aspectクラス(~)]でBCLにゴッソリ指定出来たりするのが恐ろしい。

めっぽう気になってしかたがないのがCompositionAspect。これってDIなんですかね?

フック出来るタイミングや、シリアライズのカスタマイズやら、プラグイン(ビルド後の処理時にだと思われるけどよく分かってない)、msbuild実行されないASP.NETの対応なんかもあって、イロイロ遊べそうな感じデス!