[AOP]Implementation AOP by Castle.Windsor

  • 8185
  • 0
  • 2018-11-05

[AOP]Implementation AOP by Castle.Windsor

前言

最近剛好公司的 training 在提 Decorator Pattern ,而每個月一次的讀書會最近也剛好提到了 IoCAOP ,雖然這個題材在之前已經寫過幾篇使用 Spring.NET 來實作 AOP ,但蠻多朋友反應對 Spring.NET 不熟,而且 Spring.Net 太大一包,對團隊開發或既有系統的改變太大。也的確在業界使用 .NET 開發的公司,DI/AOP framework 採 Spring.NET 的機會應該算是頗低。

因此,怎麼用比較單純的例子,以及輕量化的 library 來說明與實作 AOP ,就是這篇文章的重點。

原本想用 Autofac 當例子,但就我所知,許多大包的 IoC/AOP framework 骨子裡都是使用 Castle Project 裡面的 Dynamic Proxy ,因此決定從最根本挖掘起,練習只用 Castle 來做 AOP。

這篇文章會先從個簡單的訂單資料更新範例開始,介紹 Decorator 的實作,以及對應的問題,進而透過 AOP 的攔截器(interceptor)來解決。這篇不會介紹到 Decorator 與 AOP 的概念,有興趣的讀者可以自行參閱先前的文章:

  1. [.NET]重構之路系列v12 –用 Decorator Pattern 讓職責分離與組合
  2. [Design Pattern]Decorator Pattern with IComparer - 先比這個,再比那個
  3. Spring.NET AOP 系列

文章大綱

  1. 原始範例
  2. 使用 Decorator 加上 Log 功能
  3. 使用工廠模式動態決定是否加上 Log
  4. 套用 DI framework ,改寫工廠實作內容
  5. Decorator 可能碰到的問題
  6. AOP - 實作攔截器,透過 attribute(或稱 annotation)加上 Log 功能
  7. 針對特訂規則(如 type 或 method 符合條件)才加上攔截器

原始範例

首先在一個 Console Project 建立一個 Order 的類別,上面有 Update 與 Delete 的方法。在 Main() 裡面呼叫 Order 的 Update()  與 Delete() 。如下所示:

    class Program
    {
        static void Main(string[] args)
        {
            Order order = new Order();
            order.Update("91", "Joey");
            order.Delete("92");
        }
    }

    public class Order
    {
        public int Update(string id, string name)
        {
            Console.WriteLine("Update order, id={0}, name={1}", id, name);
            return 1;
        }

        public void Delete(string id)
        {
            Console.WriteLine("Delete order, id={0}", id);
        }
    }

image

 

需求異動-希望在 Update() 與 Delete() 之後加上 Log

常見的需求,希望 Order 呼叫 Update() 做完之後,要能記錄 log , Delete() 也是如此。而記錄 log 的動作,與 Order 的商業邏輯(屬於 service 的部分)沒有關係,屬於 concern 的部分。而在開發中希望遵守單一職責原則(SRP)以及關注點分離(SoC)

因此,常見的作法便是套用 Decorator Pattern 來分離紀錄 log 與 Order 本身的職責。程式碼修改後如下:

    internal class Program
    {
        private static void Main(string[] args)
        {
            //Order order = new Order();            
            IOrder order = new LogOrder(new Order());

            order.Update("91", "Joey");
            order.Delete("92");
        }
    }

    public class LogOrder : IOrder
    {
        private IOrder _order;

        public LogOrder(IOrder order)
        {
            this._order = order;
        }

        public int Update(string id, string name)
        {
            Console.WriteLine("== update log is starting ==");
            var result = this._order.Update(id, name);
            Console.WriteLine("== update log is stopping ==");
            Console.WriteLine();

            return result;
        }

        public void Delete(string id)
        {
            Console.WriteLine("== delete log is starting ==");
            this._order.Delete(id);
            Console.WriteLine("== delete log is stopping ==");
            Console.WriteLine();
        }
    }

    public interface IOrder
    {
        int Update(string id, string name);

        void Delete(string id);
    }

    public class Order : IOrder
    {
        public int Update(string id, string name)
        {
            Console.WriteLine("Update order, id={0}, name={1}", id, name);
            return 1;
        }

        public void Delete(string id)
        {
            Console.WriteLine("Delete order, id={0}", id);
        }
    }

程式碼說明:

  1. 新增一個 IOrder 的 interface, 讓 context 相依於 interface。並讓 Order 實作 IOrder 。
  2. 新增一個 LogOrder 的 class 來裝飾 Order ,並實作 IOrder ,於 Update() 與 Delete() 中,記錄 log 。
  3. Context 端,也就是 Main() 中,原本直接 new Order() 的部分,改為初始化 LogOrder ,並傳入 Order 的 instance, 代表用 Log 裝飾這個 Order 物件。

這樣一來就能避免, Log 的功能或程式碼,弄髒了 Order 的商業邏輯。執行結果如下:

image

 

需求異動-動態決定要不要加上 Log

這個需求很簡單,先把 IOrder 的生成方式封裝到簡單工廠中,如果需要加入 Log, 則用裝飾者的方式,若不需要,則直接回傳原本的 Order 。程式碼如下:

    internal class Program
    {
        private static void Main(string[] args)
        {
            //IOrder order = new LogOrder(new Order());
            IOrder order = Factory.GetOrderInstance();

            order.Update("91", "Joey");
            order.Delete("92");

            IOrder order2 = Factory.GetOrderInstance();

            order2.Update("91", "Joey");
            order2.Delete("92");
        }
    }

    internal class Factory
    {
        internal static IOrder GetOrderInstance()
        {
            Console.WriteLine("請輸入true或false,決定是否啟用Log");
            var isLogEnabled = Boolean.Parse(Console.ReadLine());

            if (isLogEnabled)
            {
                return new LogOrder(new Order());
            }
            else
            {
                return new Order();
            }
        }
    }

    public class LogOrder : IOrder
    {
        private IOrder _order;

        public LogOrder(IOrder order)
        {
            this._order = order;
        }

        public int Update(string id, string name)
        {
            Console.WriteLine("== update log is starting ==");
            var result = this._order.Update(id, name);
            Console.WriteLine("== update log is stopping ==");
            Console.WriteLine();

            return result;
        }

        public void Delete(string id)
        {
            Console.WriteLine("== delete log is starting ==");
            this._order.Delete(id);
            Console.WriteLine("== delete log is stopping ==");
            Console.WriteLine();
        }
    }

    public interface IOrder
    {
        int Update(string id, string name);

        void Delete(string id);
    }

    public class Order : IOrder
    {
        public int Update(string id, string name)
        {
            Console.WriteLine("Update order, id={0}, name={1}", id, name);
            return 1;
        }

        public void Delete(string id)
        {
            Console.WriteLine("Delete order, id={0}", id);
        }
    }

第一個輸入 true ,第二個輸入 false ,執行結果如下:

image

讀者可以想見,如果把這個 flag 放到 config 或 DB 中,就可以動態的決定目前的 process 是否要對 Order 的 Update() 與 Delete() 動作加上 Log ,而不需要修改到程式碼。

 

套用 DI framework 決定生成 Decorator 的方式

接下來介紹一下,如果引入 DI framework ,生成物件的方式會如何改變。

首先先透過 NuGet 安裝「Castle.Windsor」。

image

接著加入 Container 的初始化與註冊的類別,將工廠裡面的方法改從 Container 取得物件。程式碼如下所示:

    internal class Program
    {
        private static void Main(string[] args)
        {
            // 加上 IoC container 註冊與初始化
            CastleConfig.Initialized();

            IOrder order = Factory.GetOrderInstance();
            order.Update("91", "Joey");
            order.Delete("92");

            IOrder order2 = Factory.GetOrderInstance();
            order2.Update("91", "Joey");
            order2.Delete("92");
        }
    }

    internal static class CastleConfig
    {
        public static IWindsorContainer Container;

        internal static void Initialized()
        {
            Container = new WindsorContainer();

            Container.Register(
                Component.For<IOrder>()
                .ImplementedBy<Order>().LifestyleTransient());

            Container.Register(
              Component.For<IOrder>()
              .Instance(new LogOrder(Container.Resolve<IOrder>())).Named("logOrder").LifestyleTransient());
        }
    }

    internal class Factory
    {
        internal static IOrder GetOrderInstance()
        {
            Console.WriteLine("請輸入true或false,決定是否啟用Log");
            var isLogEnabled = Boolean.Parse(Console.ReadLine());

            if (isLogEnabled)
            {
                //return new LogOrder(new Order());
                return CastleConfig.Container.Resolve<IOrder>("logOrder");
            }
            else
            {
                //return new Order();
                return CastleConfig.Container.Resolve<IOrder>();
            }
        }
    }

Container 的註冊沒有什麼特別的,第一段是註冊預設碰到在生成物件的過程,若要取得 IOrder 的 instance ,預設回傳 Order 。而第二段是先透過 key: “logOrder”來註冊,當欲取得 IOrder 的實體,傳入 key 為 logOrder 時,則回傳 LogOrder 的 Decorator 。

跑起來的結果跟上面沒有引入 IoC 的例子一模一樣,而我們異動的部分只有兩個:

  1. 初始化 DI container 的動作。(只需要加一次,之後都不需要再改變)
  2. 修改工廠取得物件的方式。

Context 端的流程完全不需要異動,這符合了開放封閉原則(OCP)。

 

需求異動-除了 Order 以外,假設 Customer 物件也要記錄 Log

通常記錄 log 的需求,不會只有 Order 物件需要記錄 log ,如果現在多了一個 Customer 的物件,呼叫其 Contact() 也需要記錄 log 時,該怎麼辦?

因為 Decorator 需要實作 interface, 而 Order 與 Customer 的 interface 不會相同,那該怎麼辦?為每一個需要裝飾 Log 的物件,加上對應的 Decorator 。土法煉鋼的程式碼如下:

    internal class Program
    {
        private static void Main(string[] args)
        {
            // 加上 IoC container 註冊與初始化
            CastleConfig.Initialized();

            IOrder order = Factory.GetOrderInstance();
            order.Update("91", "Joey");
            order.Delete("92");

            ICustomer customer = Factory.GetCustomerInstance();
            customer.Contact();
        }
    }

    public class LogCustomer : ICustomer
    {
        private ICustomer _customer;

        public LogCustomer(ICustomer customer)
        {
            this._customer = customer;
        }

        public void Contact()
        {
            Console.WriteLine("== Contact log is starting ==");
            this._customer.Contact();
            Console.WriteLine("== Contact log is stopping ==");
            Console.WriteLine();
        }
    }

    public interface ICustomer
    {
        void Contact();
    }

    public class Customer : ICustomer
    {
        public void Contact()
        {
            Console.WriteLine("contact customer...");
        }
    }

    internal static class CastleConfig
    {
        public static IWindsorContainer Container;

        internal static void Initialized()
        {
            Container = new WindsorContainer();

            // 透過 key 來決定取回的 IOrder 物件為何
            Container.Register(
                Component.For<IOrder>()
                .ImplementedBy<Order>().LifestyleTransient());

            Container.Register(
              Component.For<IOrder>()
              .Instance(new LogOrder(Container.Resolve<IOrder>())).Named("logOrder").LifestyleTransient());

            // 可以透過註冊的順序,直接決定 LogCustomer Decorator 的生成方式
            Container.Register(
                Component.For<ICustomer>()
                .ImplementedBy<LogCustomer>().LifestyleTransient());

            Container.Register(
                Component.For<ICustomer>()
                .ImplementedBy<Customer>().LifestyleTransient());
        }
    }

    internal class Factory
    {
        internal static IOrder GetOrderInstance()
        {
            Console.WriteLine("請輸入true或false,決定是否啟用Log");
            var isLogEnabled = Boolean.Parse(Console.ReadLine());

            if (isLogEnabled)
            {
                //return new LogOrder(new Order());
                return CastleConfig.Container.Resolve<IOrder>("logOrder");
            }
            else
            {
                //return new Order();
                return CastleConfig.Container.Resolve<IOrder>();
            }
        }

        internal static ICustomer GetCustomerInstance()
        {
            // 直接回傳Log裝飾過的Customer
            return CastleConfig.Container.Resolve<ICustomer>();
        }
    }

程式碼說明如下:

  1. 一樣新增一個 ICustomer 介面,一個 Customer 類別,一個 LogCustomer 類別。透過 Decorator Pattern 來滿足需求。
  2. Container 註冊的部分,展現了另外一種方式,當呼叫端要取得 ICustomer 物件,我們希望都是回傳 Decorator 時,可以直接先註冊 LogCustomer ,再註冊 Customer 給 ICustomer ,這樣一來取得的順序就會是:要取得 ICustomer ,發現要取得 LogCustomer ,在取得 LogCustomer 發現還要傳入 ICustomer ,則以 Customer 傳入。有點類似 new LogCusomter(new Customer()) 的味道。

image

回到原本提的問題,Log 這個 concern 可能存在很多地方都要使用,可不可以不要讓 Log 被 Decorator 的介面綁住。

所以,要透過 AOP framework 的 interceptor 來設計。

 

AOP-攔截器的實作

首先設計一個 LogInterceptor 類別,並實作 Castle.DynamicProxy.IInterceptor 介面。程式碼如下:

   internal class LogInterceptor : IInterceptor
    {
        public void Intercept(IInvocation invocation)
        {
            var typeName = invocation.TargetType.FullName;
            var methodName = invocation.Method.Name;

            Console.WriteLine("== {0}.{1} log is starting ==", typeName, methodName);

            foreach (var arg in invocation.Arguments)
            {
                Console.WriteLine("argument:{0}", arg);
            }

            invocation.Proceed();

            Console.WriteLine("return value:{0}", invocation.ReturnValue ?? "null");
            Console.WriteLine("== {0}.{1} log is stopping ==", typeName, methodName);
            Console.WriteLine();
        }
    }

IInterceptor 介面只有一個 Intercept 的方法,而傳入的參數就是在呼叫時,把所有要攔截的目標與方法抽象成 IInvocation 介面。上面有幾個常用的關鍵:

  1. invocation.TargetType:也就是攔截的目標型別。
  2. invocation.Method:也就是這次攔截到的方法。
  3. invocation.Arguments:呼叫目標物件的方法,所傳入的參數。
  4. invocation.Proceed():實際呼叫目標物件的方法。
  5. invocation.ReturnValue:實際目標方法的回傳值,可於攔截器中重新設定想要回傳的值。

接著只需要在 Container 註冊這個攔截器的型別,最後,在要攔截的物件類別上,加載 Interceptor Attribute 即可。

Container 註冊的程式碼如下所示:

    internal static class CastleConfig
    {
        public static IWindsorContainer Container;

        internal static void Initialized()
        {
            Container = new WindsorContainer();

            // 註冊攔截器的型別與物件供 Interceptor attribute 使用
            Container.Register(
                Component.For<LogInterceptor>()
                .ImplementedBy<LogInterceptor>().LifestyleTransient());

            Container.Register(
                Component.For<IOrder>()
                .ImplementedBy<Order>().LifestyleTransient());

            Container.Register(
                Component.For<ICustomer>()
                .ImplementedBy<Customer>().LifestyleTransient());
        }
    }

程式碼說明:

  1. Container 註冊,註冊 LogInterceptor 的型別對應。
  2. 其他的註冊則採預設的 Order 對應 IOrder ,Customer 對應 ICustomer 。

Customer 與 Order 的類別如下:

    [Interceptor(typeof(LogInterceptor))]
    public class Customer : ICustomer
    {
        public void Contact()
        {
            Console.WriteLine("contact customer...");
        }
    }

    [Interceptor(typeof(LogInterceptor))]
    public class Order : IOrder
    {
        public int Update(string id, string name)
        {
            Console.WriteLine("Update order, id={0}, name={1}", id, name);
            return 1;
        }

        public void Delete(string id)
        {
            Console.WriteLine("Delete order, id={0}", id);
        }
    }

假設現在先不動態決定要不要加入 Log 的功能,而是只要有標記 Interceptor Attribute,就要加上 Log ,則工廠類別也可以最單純化地取回介面所對應的實體物件執行個體,程式碼如下:

    internal class Factory
    {
        internal static IOrder GetOrderInstance()
        {
            //Console.WriteLine("請輸入true或false,決定是否啟用Log");
            //var isLogEnabled = Boolean.Parse(Console.ReadLine());

            //if (isLogEnabled)
            //{
            //    //return CastleConfig.Container.Resolve<IOrder>("logOrder");
            //    return CastleConfig.Container.Resolve<IOrder>();
            //}
            //else
            //{
            //    return CastleConfig.Container.Resolve<IOrder>();
            //}
            
            return CastleConfig.Container.Resolve<IOrder>();
        }

        internal static ICustomer GetCustomerInstance()
        {            
            return CastleConfig.Container.Resolve<ICustomer>();
        }
    }

Main() 方法完全不需要改變,程式碼與執行結果如下:

        private static void Main(string[] args)
        {
            // 加上 IoC container 註冊與初始化
            CastleConfig.Initialized();

            IOrder order = Factory.GetOrderInstance();
            order.Update("91", "Joey");
            order.Delete("92");

            ICustomer customer = Factory.GetCustomerInstance();
            customer.Contact();
        }

image

可以看到,只需要在類別上加上 [Interceptor(typeof(LogIntercepter))]就可以為該類別上所有方法加上攔截器。在 runtime 取回對應物件時,AOP framework 偵測到該物件有標記 Interceptor Attribute 時,就會動態加載攔截器上去,這也就是 Dynamic Decorator 的效果。

這樣就可以避免為了要用 Decorator 而撰寫多份的 LogDecorator 物件,反而導致違反了 DRY 原則的壞味道。

 

如何根據條件決定要不要加載攔截器

這邊的條件列出兩種,讓讀者知道 AOP framework 可以做到什麼樣的地步。

  1. 原本的工廠,要能動態決定回來的 Order 要不要有 Log 攔截器。
  2. 當方法名稱中含有 Update 的字眼時,才加載 Log 攔截器。

第一個條件的部分,只需要在 Container 註冊時,額外註冊一個有加掛攔截器的 key 即可。 Container 程式碼修改如下:

    internal static class CastleConfig
    {
        public static IWindsorContainer Container;

        internal static void Initialized()
        {
            Container = new WindsorContainer();

            // 註冊攔截器的型別與物件供 Interceptor attribute 使用
            Container.Register(
                Component.For<LogInterceptor>()
                .ImplementedBy<LogInterceptor>().LifestyleTransient());

            Container.Register(
                Component.For<IOrder>()
                .ImplementedBy<Order>().LifestyleTransient());

            // 額外註冊有加載攔截器的Order
            Container.Register(
                Component.For<IOrder>()
                .ImplementedBy<Order>().LifestyleTransient().Named("logOrder")
                .Interceptors(InterceptorReference.ForType<LogInterceptor>()).Anywhere);

            Container.Register(
                Component.For<ICustomer>()
                .ImplementedBy<Customer>().LifestyleTransient()
                .Interceptors(InterceptorReference.ForType<LogInterceptor>()).Anywhere);
        }
    }

工廠修改如下:

    internal class Factory
    {
        internal static IOrder GetOrderInstance()
        {
            Console.WriteLine("請輸入true或false,決定是否啟用Log");
            var isLogEnabled = Boolean.Parse(Console.ReadLine());

            if (isLogEnabled)
            {
                return CastleConfig.Container.Resolve<IOrder>("logOrder");
            }
            else
            {
                return CastleConfig.Container.Resolve<IOrder>();
            }
        }

        internal static ICustomer GetCustomerInstance()
        {
            return CastleConfig.Container.Resolve<ICustomer>();
        }
    }

接下來 Main() 方法回到最原本取兩個 Order 物件的情況,程式碼與執行結果如下:

    internal class Program
    {
        private static void Main(string[] args)
        {
            // 加上 IoC container 註冊與初始化
            CastleConfig.Initialized();

            IOrder order = Factory.GetOrderInstance();
            order.Update("91", "Joey");
            order.Delete("92");

            IOrder order2 = Factory.GetOrderInstance();
            order2.Update("91", "Joey");
            order2.Delete("92");

            Console.WriteLine();

            ICustomer customer = Factory.GetCustomerInstance();
            customer.Contact();
        }
    }

image

記得要把 Order 與 Customer 上的 InterceptorAttribute 移除

第二個條件則是,只針對方法名字有 Update 字眼的進行攔截,這比較麻煩一些些,但卻很能代表 AOP 的強大彈性之處。

一樣,只需要修改 Container 註冊的部分,就能讓這些需求都滿足。

只需修改 Container 註冊的部分,這是一個很重要很重要的關鍵,因為這樣才完全地滿足了開放封閉原則(OCP),動態的組裝各個僅擁有單一職責的物件,卻能滿足使用端動態的需求。

Container 修改後的程式碼如下:

    internal static class CastleConfig
    {
        public static IWindsorContainer Container;

        internal static void Initialized()
        {
            Container = new WindsorContainer();

            // 註冊攔截器的型別與物件供 Interceptor attribute 使用
            Container.Register(
                Component.For<LogInterceptor>()
                .ImplementedBy<LogInterceptor>().LifestyleTransient());

            Container.Register(
                Component.For<IOrder>()
                .ImplementedBy<Order>().LifestyleTransient());

            // 額外註冊有加載攔截器的Order
            //Container.Register(
            //    Component.For<IOrder>()
            //    .ImplementedBy<Order>().LifestyleTransient().Named("logOrder")
            //    .Interceptors(InterceptorReference.ForType<LogInterceptor>()).Anywhere);

            Container.Register(
                Component.For<IOrder>()
                .ImplementedBy<Order>().LifestyleTransient().Named("logOrder")
                .Interceptors(InterceptorReference.ForType<LogInterceptor>()).Anywhere
                .SelectInterceptorsWith(new OnlyUpdateMethodBeSelected()));

            Container.Register(
                Component.For<ICustomer>()
                .ImplementedBy<Customer>().LifestyleTransient()
                .Interceptors(InterceptorReference.ForType<LogInterceptor>()).Anywhere);
        }
    }

    internal class OnlyUpdateMethodBeSelected : IInterceptorSelector
    {
        public IInterceptor[] SelectInterceptors(Type type, System.Reflection.MethodInfo method, IInterceptor[] interceptors)
        {
            if (method.Name.Contains("Update"))
            {
                return interceptors;
            }
            else
            {
                return Enumerable.Empty<IInterceptor>().ToArray();
            }
        }
    }

程式碼說明:

  1. 在原本註冊 Order 要加載攔截器的部分,加上一個 SelectedInterceptorsWith() ,代表只有哪一些符合條件的攔截器要被加載上去,這時傳入的參數型別為 IInterceptorSelector
  2. 自訂一個 OnlyUpdateMethodBeSelected 的類別實作 IInterceptorSelector 介面。
  3. 當方法名字有包含 Update 時,則把上面所有攔截器都加上去。否則,則卸載所有攔截器。

執行結果可以發現,只有 Update() 方法被加載 Log 的攔截器了,或是說,只有 Update() 方法時,會讓某些攔截器生效。如下:

image

這邊只是個簡單的範例,讀者們可以看到 IInterceptorSelector 會傳入 type 與 method ,也就代表開發人員能從 type 與 method 上透過 reflection 做相當相當多的事情,傳入的 interceptors 也可以透過 LINQ 去判斷什麼條件下,希望加載什麼 interceptors 。

 

結論

這篇文章雖然略過了基礎概念不提,但還是完整的從最原始的需求,以及需求的變化,一路帶到 Design Patterns 的應用,以及 DI/AOP framework 可以協助我們解決什麼樣的問題。

以下則摘要幾個重點:

  1. 大部分的 AOP framework 都 base on DI framework 。如果你的團隊跟系統還沒導入 DI framework ,那麼建議讀者最好可以先實作工廠模式,讓所有生成物件的部分封裝起來,未來只要搭配策略模式(Strategy Pattern)多型的概念,就能把實作面的商業邏輯與 context 流程分開。屆時只需要把工廠內的實作部分,接上 DI framework 即可。而一旦有 DI framework 的情況,AOP 就能滿足你絕大部分關注點分離(SoC)的開發與彈性的需求。
  2. 不管需求怎麼異動,只要原本的 context 流程沒改,商業邏輯沒變,那我們希望就只需要動到工廠內部或是 Container 註冊的部分,即可滿足新的需求。這也是為什麼開放封閉原則是 SOLID 原則中的總綱。
  3. 原本可能弄髒商業邏輯或重複開發的 concern 模組,例如:權限檢查、Log紀錄、交易控管、例外處理等等,都可以透過攔截器的方式來設計,並可以在線上環境動態的決定是否加載,以滿足特定的需求。

最後有兩個附註說明,第一,DI framework 註冊與取用物件的部分,也可以透過 XML/Config 方式來設計。第二,看起來 Castle 雖然輕量化,但還是有一些地方用起來不夠順手,這就是為什麼會有額外許多 DI framework ,如 Autofac ,要基於 Castle project 再往上加上很多包裝,因為可以讓開發人員更簡易、好懂、彈性的操作 DI 與 AOP 的設計。

 

Sample Code 下載: DecoratorToAop.zip


blog 與課程更新內容,請前往新站位置:http://tdd.best/