首頁 > 軟體

C#程式設計之AOP程式設計思想

2022-03-18 13:01:03

一、什麼是AOP

AOP:Aspect Oriented Programming的縮寫,意為面向切面程式設計,通過預編譯方式和執行期間動態代理實現程式功能的統一維護的一種技術。AOP是OOP思想的延續。利用AOP可以對業務邏輯的各個部分進行隔離,從而使得業務邏輯各部分之間的耦合度降低,提高程式的可重用性,同時提高了開發的效率。

為什麼要學習AOP呢?

AOP的應用場景非常廣泛,在一些高階工程師或者架構師的面試過程中,頻率出現的比較多。

二、程式設計思想的發展路線

1、POP

POP:Procedure Oriented Programming的縮寫,即程式導向程式設計,是一種以過程為中心的程式設計思想。

程式導向是分析出解決問題的步驟,然後用函數或者方法,把這些步驟一步一步的實現,使用的時候在一個一個的一次呼叫函數或者方法,這就是程式導向程式設計。最開始的時候都是程式導向程式設計。程式導向是最為實際的一種思考方式。就算是物件導向程式設計,裡面也是包含有程式導向的程式設計思想,因為程式導向是一種基礎的程式設計思考方式,它從實際出發來考慮如何實現需求。

POP的不足:程式導向程式設計,只能處理一些簡單的問題,無法處理一些複雜的問題。如果問題很複雜,全部以流程來思考的話,會發現流程很混亂,甚至流程都不能進行下去。

2、OOP

OOP:Object Oriented Programming的縮寫,即物件導向程式設計。

早期的計算機程式設計都是程式導向的,因為早期的程式設計都比較簡單。但是隨著時間的發展,需求處理的問題越來越多,問題就會越來越複雜,這時就不能簡單的使用程式導向的程式設計了,就出現了物件導向程式設計。在計算機裡面,把所有的東西都想象成一種事物。現實世界中的物件都有一些屬性和行為,也就對應計算機中的屬性和方法。

物件導向程式設計就是把構成問題的事物分解成各個物件,建立物件的目的不是為了完成一個步驟,而是為了描述某個事物在整個解決問題的步驟中的行為。

我們以一棟大樓為例來說明OOP的不足。

我們把系統比喻成一棟樓,類或者物件是磚塊,磚塊組成了一面牆,多面牆構成一間房間,多個房間構成一棟樓。
這就好比一個模組的功能是由多個類實現,模組又組成了某一項服務,多個服務構成了一個完整的系統。一個系統開發完成並不代表就真的完成了,以後肯定會有各種需求的變更出現,需求變更出現以後就要去修改程式碼,程式碼都在類裡面,這就相當於去修改類。如果是小範圍的修改影響還不是很大,如果是大範圍的修改,影響就比較大了。即使每次修改都很小,但是如果經常進行修改,影響也會很大。就會造成系統的不穩定。我們得出結論:類應該是固定的,不應該頻繁的去修改,甚至是不允許修改。這也是為什麼有那麼多的設計原則和設計模式。大部分的設計模式都是為了解決這類問題的,即在不修改類的前提下去擴充套件功能。

OOP的不足:產生新的需求會導致程式程式碼不斷的進行修改,容易造成程式的不穩定。

如果非常瞭解OOP的話,那麼我們應該知道,從物件的組織角度來講,分類方法都是以繼承關係為主線的,我們稱為縱向。如果只使用OOP思想的話,會帶來兩個問題:
1、共性問題。
2、擴充套件問題,需要對先有類進行擴充套件時就比較困難了。

OOP與POP的區別:

在對比程式導向的時候,物件導向的方法是把事物最小化為物件,包括屬性和方法。當程式的規模比較小的時候,程式導向程式設計還是有一些優勢的,因為這時候程式的流程是比較容易梳理清楚的。以早上去上班為例,過程就是起床、穿衣、刷牙洗臉、去公司。每一步都是按照順序完成的,我們只需要按照步驟去一步一步的實現裡面的方法就行了,最後在依次呼叫實現的方法即可,這就是程式導向開發。

如果使用物件導向程式設計,我們就需要抽象出來一個員工類,該員工具有起床、穿衣、刷牙洗臉、去公司的四個方法。但是,最終要實現早上去上班的這個需求的話,還是要按照順序依次來呼叫四個方法。最開始的時候,我們是按照程式導向的思想來思考該需求,然後在按照物件導向的思想來抽象出幾個方法,最終要實現這個需求,還是要按照程式導向的順序來實現。

物件導向和程式導向的區別僅僅是在思考問題方式上面的不同。最終你會發現,在你實現這個需求的時候,即使使用了物件導向的思想抽象出來了員工類,但是最後還是要使用程式導向來實現這個需求。

3、AOP

AOP:Aspect Oriented Programming的縮寫,即面向切面程式設計。是對OOP的一種補充,在不修改原始類的情況下,給程式動態新增統一功能的一種技術。

OOP關注的是將需求功能劃分為不同的並且相對獨立、封裝良好的類,依靠繼承和多型來定義彼此的關係。AOP能夠將通用需求功能從不相關的類中分離出來,很多類共用一個行為,一旦發生變化,不需要去修改很多類,只需要去修改這一個類即可。

AOP中的切面是指什麼呢?切面指的是橫切關注點。看下面一張圖:

OOP是為了將狀態和行為進行模組化。上圖是一個商場系統,我們使用OOP將該系統縱向分為訂單管理、商品管理、庫存管理模組。在該系統裡面,我們要進行授權驗證。像訂單、商品、庫存都是業務邏輯功能,但是這三個模組都需要一些共有的功能,比如說授權驗證、紀錄檔記錄等。我們不可能在每個模組裡面都去寫授權驗證,而且授權驗證也不屬於具體的業務,它其實屬於功能性模組,並且會橫跨多個業務模組。可以看到這裡是橫向的,這就是所謂的切面。通俗的將,AOP就是將公用的功能給提取出來,如果以後這些公用的功能發生了變化,我們只需要修改這些公用功能的程式碼即可,其它的地方就不需要去更改了。所謂的切面,就是隻關注通用功能,而不關注業務邏輯,而且不修改原有的類。

AOP優勢:

  •  將通用功能從業務邏輯中抽離出來,提高程式碼複用性,有利於後期的維護和擴充套件。
  • 軟體設計時,抽出通用功能(切面),有利於軟體設計的模組化,降低軟體架構的複雜度。

AOP的劣勢:

  • AOP的對OOP思想的一種補充,它無法單獨存在。如果說單獨使用AOP去設計一套系統是不可能的。在設計系統的時候,如果系統比較簡單,那麼可以只使用POP或者OOP來設計。如果系統很複雜,就需要使用AOP思想。首先要使用POP來梳理整個業務流程,然後根據POP的流程,去整理類和模組,最後在使用AOP來抽取通用功能。

AOP和OOP的區別:

  • 面向目標不同:OOP是面向名詞領域(抽象出來一個事物,比如學生、員工,這些都是名詞)。AOP是面向動詞領域(比如鑑權、記錄紀錄檔,這些都是動作或行為)。
  • 思想結構不同:OOP是縱向的(以繼承為主線,所以是縱向的)。AOP是橫向的。
  • 注重方面不同:OOP是注重業務邏輯單元的劃分,AOP偏重業務處理過程中的某個步驟或階段。

POP、OOP、AOP三種思想是相互補充的。在一個系統的開發過程中,這三種程式設計思想是不可或缺的。

三、實現AOP

我們在上面講解了有關AOP的一些理論知識,那麼如何在程式碼裡面實現呢?

實現AOP有兩種方式:

  • 靜態代理實現。所謂靜態代理,就是我們自己來寫代理物件。
  • 動態代理實現。所謂動態代理,就是在程式執行時,去生成一個代理物件。

1、靜態代理

實現靜態代理需要使用到兩種設計模式:裝飾器模式和代理模式。

裝飾器模式:允許向一個現有的物件新增新的功能,同時又不改變這個現有物件的結構。屬於結構型設計模式,它是作為現有類的一種包裝。首先會建立一個裝飾類,用來包裝原有的類,並在保持類的完整性的前提下,提供額外的功能。看下面的例子。

我們首先建立一個User類:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace StaticDemo.Model
{
    public class User
    {
        public string Name { get; set; }
        public string Password { get; set; }
    }
}

接著我們建立一個賬號服務的介面,裡面有一個方法,用來註冊一個使用者:

using StaticDemo.Model;

namespace StaticDemo.Services
{
    /// <summary>
    /// 介面
    /// </summary>
    public interface IAccountService
    {
        /// <summary>
        /// 註冊使用者
        /// </summary>
        /// <param name="user"></param>
        void Reg(User user);
    }
}

然後建立一個類來實現上面的介面:

using StaticDemo.Model;
using System;

namespace StaticDemo.Services
{
    /// <summary>
    /// 實現IAccountService介面
    /// </summary>
    public class AccountService : IAccountService
    {
        public void Reg(User user)
        {
            // 業務程式碼 之前 或者之後執行一些其它的邏輯
            Console.WriteLine($"{user.Name}註冊成功");
        }
    }
}

我們在建立一個裝飾器類:

using StaticDemo.Model;
using StaticDemo.Services;
using System;

namespace StaticDemo
{
    /// <summary>
    /// 裝飾器類
    /// </summary>
    public class AccountDecorator : IAccountService
    {
        private readonly IAccountService _accountService;

        public AccountDecorator(IAccountService accountService)
        {
            _accountService = accountService;
        }

        public void Reg(User user)
        {
            Before();
            // 這裡呼叫註冊的方法,原有類裡面的邏輯不會改變
            // 在邏輯前面和後面分別新增其他邏輯
            _accountService.Reg(user);
            After();
        }

        private void Before()
        {
            Console.WriteLine("註冊之前的邏輯");
        }

        private void After()
        {
            Console.WriteLine("註冊之後的邏輯");
        }
    }
}

我們會發現裝飾器類同樣實現了IAccountService介面。最後我們在Main方法裡面呼叫:

using StaticDemo.Model;
using StaticDemo.Services;
using System;

namespace StaticDemo
{
    class Program
    {
        static void Main(string[] args)
        {
            // 範例化物件
            IAccountService accountService = new AccountService();
            // 範例化裝飾器類,並用上面的範例給構造方法傳值
            var account = new AccountDecorator(accountService);
            var user = new User { Name = "Rick", Password = "12345678" };
            // 呼叫裝飾器類的註冊方法,相當於呼叫範例化物件的註冊方法
            account.Reg(user);

            Console.ReadKey();
        }
    }
}

執行結果:

下面我們在來看看如何使用代理模式實現。

代理模式:即一個類代表另一個類的功能。我們會建立一個代理類,這個代理類和裝飾器類基本一樣。看一下程式碼:

using StaticDemo.Model;
using StaticDemo.Services;
using System;

namespace StaticDemo
{
    /// <summary>
    /// 代理類
    /// </summary>
    public class ProxyAccount : IAccountService
    {
        private readonly IAccountService _accountService;

        /// <summary>
        /// 建構函式沒有引數
        /// 直接在裡面建立了AccountService類
        /// </summary>
        public ProxyAccount()
        {
            _accountService = new AccountService();
        }

        public void Reg(User user)
        {
            before();
            _accountService.Reg(user);
            after();
        }

        private void before()
        {
            Console.WriteLine("代理:註冊之前的邏輯");
        }

        private void after()
        {
            Console.WriteLine("代理:註冊之後的邏輯");
        }
    }
}

Main方法裡面呼叫:

using StaticDemo.Model;
using StaticDemo.Services;
using System;

namespace StaticDemo
{
    class Program
    {
        static void Main(string[] args)
        {
            #region 裝飾器模式
            //// 範例化物件
            //IAccountService accountService = new AccountService();
            //// 範例化裝飾器類,並用上面的範例給構造方法傳值
            //var account = new AccountDecorator(accountService);
            //var user = new User { Name = "Rick", Password = "12345678" };
            //// 呼叫裝飾器類的註冊方法,相當於呼叫範例化物件的註冊方法
            //account.Reg(user);
            #endregion

            #region 代理模式
            var account = new ProxyAccount();
            var user = new User { Name = "Tom", Password = "12345678" };
            account.Reg(user);
            #endregion

            Console.ReadKey();
        }
    }
}

執行結果:

可能有的人會發現,裝飾器類和代理類很相像,功能也一模一樣,僅僅是建構函式不同。那麼裝飾器模式和代理模式有區別嗎?有些東西,形式上看起來區別很小,但實際上他們區別很大。它們在形式上確實一樣,不管是裝飾器類還是代理類,它們都要實現相同的介面,但是它們在運用的時候還是有區別的。

裝飾器模式關注於在一個物件上動態新增方法,而代理模式關注於控制物件的存取。簡單來說,使用代理模式,我們的代理類可以隱藏一個類的具體資訊。var account = new ProxyAccount();僅看這段程式碼不看原始碼,不知道里面代理的是誰。

當使用代理模式的時候,我們常常是在代理類中去建立一個物件的範例:_accountService = new AccountService()。而當我們使用裝飾器模式的時候,我們通常是將原始物件作為一個引數傳遞給裝飾器的建構函式。簡單來說,在使用裝飾器模式的時候,我們可以明確地知道裝飾的是誰,而且更重要的是,代理類裡面是寫死的,在編譯的時候就確定了關係。而裝飾器是在執行時來確定的。

2、動態代理

動態代理實現也有兩種方式;

  • 通過程式碼織入的方式。例如PostSharp第三方外掛。我們知道.NET程式最終會編譯成IL中間語言,在編譯程式的時候,PostSharp會動態的去修改IL,在IL裡面新增程式碼,這就是程式碼織入的方式。
  • 通過反射的方式實現。通過反射實現的方法非常多,也有很多實現了AOP的框架,例如Unity、MVC過濾器、Autofac等。

我們先來看看如何使用PostSharp實現動態代理。PostSharp是一款收費的第三方外掛。

首先新建立一個控制檯應用程式,然後建立一個訂單業務類:

using System;

namespace PostSharpDemo
{
    /// <summary>
    /// 訂單業務類
    /// </summary>
    public class OrderBusiness
    {
        public void DoWork()
        {
            Console.WriteLine("執行訂單業務");
        }
    }
}

接著在Main方法裡面呼叫:

using System;

namespace PostSharpDemo
{
    class Program
    {
        static void Main(string[] args)
        {
            OrderBusiness order = new OrderBusiness();
            // 呼叫方法
            order.DoWork();
            Console.ReadKey();
        }
    }
}

執行結果:

這時又提出了一個新的需求,要去新增一個紀錄檔功能,記錄業務的執行情況,按照以前的辦法,需要定義一個紀錄檔幫助類:

using System;
using System.IO;

namespace PostSharpDemo
{
    public class LgoHelper
    {
        public static void RecoreLog(string message)
        {
            string strPath = AppDomain.CurrentDomain.BaseDirectory+"\log.txt";
            using(StreamWriter sw=new StreamWriter(strPath,true))
            {
                sw.WriteLine(message);
                sw.Close();
            }
        }
    }
}

如果不使用AOP,我們就需要在記錄紀錄檔的地方範例化Loghelper物件,然後記錄紀錄檔:

using System;

namespace PostSharpDemo
{
    /// <summary>
    /// 訂單業務類
    /// </summary>
    public class OrderBusiness
    {
        public void DoWork()
        {
            // 記錄紀錄檔
            LgoHelper.RecoreLog("執行業務前");
            Console.WriteLine("執行訂單業務");
            LgoHelper.RecoreLog("執行業務後");
        }
    }
}

我們再次執行程式,檢視結果:

我們看看紀錄檔內容:

這樣修改可以實現記錄紀錄檔的功能。但是上面的方法會修改原先已有的程式碼,這就違反了開閉原則。而且新增紀錄檔也不是業務需求的變動,不應該去修改業務程式碼。下面使用AOP來實現。首先安裝PostSharp,直接在NuGet裡面搜尋,然後安裝即可:

然後定義一個LogAttribute類,繼承自OnMethodBoundaryAspect。這個Aspect提供了進入、退出函數等連線點方法。另外,Aspect上必須設定“[Serializable] ”,這與PostSharp內部對Aspect的生命週期管理有關:

using PostSharp.Aspects;
using System;

namespace PostSharpDemo
{
    [Serializable]
    [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
    public class LogAttribute: OnMethodBoundaryAspect
    {
        public string ActionName { get; set; }
        public override void OnEntry(MethodExecutionArgs eventArgs)
        {
            LgoHelper.RecoreLog(ActionName + "開始執行業務前");
        }

        public override void OnExit(MethodExecutionArgs eventArgs)
        {
            LgoHelper.RecoreLog(ActionName + "業務執行完成後");
        }
    }
}

然後Log特性應用到DoWork函數上面:

using System;

namespace PostSharpDemo
{
    /// <summary>
    /// 訂單業務類
    /// </summary>
    public class OrderBusiness
    {
        [Log(ActionName ="DoWork")]
        public void DoWork()
        {
            // 記錄紀錄檔
            // LgoHelper.RecoreLog("執行業務前");
            Console.WriteLine("執行訂單業務");
            // LgoHelper.RecoreLog("執行業務後");
        }
    }
}

這樣修改以後,只需要在方法上面新增一個特性,以前記錄紀錄檔的程式碼就可以註釋掉了,這樣就不會再修改業務邏輯程式碼了,執行程式:

在看看紀錄檔:

這樣就實現了AOP功能。

我們在看看使用Remoting來實現動態代理。

首先還是建立一個User實體類:

namespace DynamicProxy.Model
{
    public class User
    {
        public string Name { get; set; }
        public string Password { get; set; }
    }
}

然後建立一個介面,裡面有一個註冊方法:

using DynamicProxy.Model;

namespace DynamicProxy.Services
{
    public interface IAccountService
    {
        void Reg(User user);
    }
}

然後建立介面的實現類:

using DynamicProxy.Model;
using System;

namespace DynamicProxy.Services
{
    public class AccountService : MarshalByRefObject, IAccountService
    {
        public void Reg(User user)
        {
            Console.WriteLine($"{user.Name}註冊成功");
        }
    }
}

然後建立一個泛型的動態代理類:

using System;
using System.Runtime.Remoting;
using System.Runtime.Remoting.Messaging;
using System.Runtime.Remoting.Proxies;

namespace DynamicProxy
{
    public class DynamicProxy<T> : RealProxy
    {
        private readonly T _target;

        // 執行之前
        public Action BeforeAction { get; set; }

        // 執行之後
        public Action AfterAction { get; set; }

        // 被代理泛型類
        public DynamicProxy(T target) : base(typeof(T))
        {
            _target = target;
        }

        // 代理類呼叫方法
        public override IMessage Invoke(IMessage msg)
        {
            var reqMsg = msg as IMethodCallMessage;
            var target = _target as MarshalByRefObject;

            BeforeAction();
            // 這裡才真正去執行代理類裡面的方法
            // target表示被代理的物件,reqMsg表示要執行的方法
            var result = RemotingServices.ExecuteMessage(target, reqMsg);
            AfterAction();
            return result;
        }

    }
}

我們看到,這個泛型動態代理類裡面有兩個泛型委託:BeforeAction、AfterAction。通過建構函式把代理泛型類傳遞進去。最後呼叫Invoke方法執行代理類的方法。

最後我們還要建立一個代理工廠類,用來建立代理物件,通過呼叫動態代理來建立動態代理物件:

using System;

namespace DynamicProxy
{
    /// <summary>
    /// 動態代理工廠類
    /// </summary>
    public static class ProxyFactory
    {
        public static T Create<T>(Action before, Action after)
        {
            // 範例化被代理泛型物件
            T instance = Activator.CreateInstance<T>();
            // 範例化動態代理,建立動態代理物件
            var proxy = new DynamicProxy<T>(instance) { BeforeAction = before, AfterAction = after };
            // 返回透明代理物件
            return (T)proxy.GetTransparentProxy();
        }
    }
}

我們最後在Main方法裡面呼叫:

using DynamicProxy.Model;
using DynamicProxy.Services;
using System;

namespace DynamicProxy
{
    class Program
    {
        static void Main(string[] args)
        {
            // 呼叫動態代理工廠類建立動態代理物件,傳遞AccountService,並且傳遞兩個委託
            var acount = ProxyFactory.Create<AccountService>(before:() =>
            {
                Console.WriteLine("註冊之前");
            }, after:() =>
            {
                Console.WriteLine("註冊之後");
            });

            User user = new User() 
            {
             Name="張三",
             Password="123456"
            };
            // 呼叫註冊方法
            acount.Reg(user);

            Console.ReadKey();
        }
    }
}

程式執行結果:

這樣就利用Remoting實現了動態代理。

GitHub地址:https://github.com/jxl1024/AOP

到此這篇關於C#程式設計之AOP程式設計思想的文章就介紹到這了。希望對大家的學習有所幫助,也希望大家多多支援it145.com。


IT145.com E-mail:sddin#qq.com