在 .net core 中實現AOP

圖片

AOP(Aspect-Oriented Programming,剖面導向程式設計)是一種軟體開發方法論,其主要目的是將商業邏輯(Business Logic)和橫切關注點(Cross-Cutting Concern, 例如 Logging, Permission , Exception Handling…etc.)分離出來,進而使系統更加職責清晰、易於維護和擴展。

在AOP中,橫切關注點被稱為切面(Aspect),切面是一種跨越多個物件和方法的模塊化程式碼,用來實現特定的橫切關注點。在AOP中,我們可以將切面應用(Apply)到一個或多個類或方法上,從而實現這些類或方法的橫切關注點。

AOP的主要概念是,若能將橫切關注點從主要商業邏輯(Business Logic)中分離出來,即可達到降低耦合、提高軟體重用性的效果。

舉例說明

首先來整理一下這個命題,我們知道AOP想把Business Logic和Infrastructure Logic做一個切割,達成cross-cutting concerns的獨立性,但這樣講很抽象,具體情境是什麼呢?

看底下這一段code:

using System;

namespace testAOP
{
    class Program
    {
        static void Main(string[] args)
        {
            //建立BMIProcessor物件
            BMIProcessor BMI = new BMIProcessor();
            BMI.Height = 170;
            BMI.Weight = 70;
            //計算BMI
            var ret = BMI.Calculate();
            //顯示
            Console.WriteLine($"BMI : {ret}");

            Console.WriteLine("Press any key for continuing...");
            Console.ReadKey();
        }
    }

    public class BMIProcessor
    {
        public int Weight { get; set; }
        public int Height { get; set; }
        public Decimal BMI
        {
            get
            {
                return Calculate();
            }
        }

        //計算BMI
        public Decimal Calculate()
        {
            Decimal result = 0;
            Decimal height = (Decimal)Height / 100;
            result = Weight / (height * height);

            return result;
        }
    }
}

BMIProcessor 是負責計算BMI的類別,上面這段程式碼的第14行使用了Calculate()這個Method來計算BMI,但這段Code有些小問題,例如,傳入身高體重是0時候,可能發生Exception。

你會想,這不要緊,我們在BMI類別的Calculate()方法中加入try…catch,類似底下這樣(當然我知道可以加上傳入參數的檢查,但我想講的點是try…catch啦,所以我們先這樣幹):

//計算BMI
public Decimal Calculate()
{
    Decimal result = 0;
    try
    {
        Decimal height = (Decimal)Height / 100;
        result = Weight / (height * height);
    }
    catch (Exception ex)
    {
        //錯誤處理
        //send mail
        //send line message...etc.
        //ex.....
    }
    return result;
}

這樣做本身沒問題,但如此一來,我們就讓計算BMI的這段code,嚴重的與Exception Handling中的『發email』或『發LINE訊息』的功能相依!

此外,倘若我們想針對Calculate()這個Method被呼叫幾次,做一個Log,你可能也會想在Calculate()這個Method中加入讀寫檔案存取log或寫入DB的Code,這下不得了,簡單的BMI計算(business logic)開始與檔案系統、DB、發mail…等等(這些我們稱為 Infrastructure logic的程式碼)相依。

這邊定義的相依,具體而言就是,只要你用到了該method,就勢必得一起用到DB、檔案、發mail…等機制。

而你的程式碼中肯定也不只這一個地方需要進行Log或是exception handling機制,而是有好幾個方法都需要用戶,那就更精采了…

我們的程式碼可能開始會變成這樣…

private void MethodA()
{
    try
    {
        //Business Logic block
        //Business Logic block
        //Business Logic block
    }
    catch (Exception)
    {
        //完全一樣的exception Handling block
    }
}

private void MethodB()
{
    try
    {
        //另一段Business Logic block
        //另一段Business Logic block
        //另一段Business Logic block
    }
    catch (Exception)
    {
        //完全一樣的exception Handling block
    }
}

有沒有發現,其實一般我們在寫每一段程式碼的過程當中,都有除了Business Logic以外,圍繞著實現主需求的其他很多事情,其實並非核心的運算(例如上面的catch區塊…),仔細想想,這樣的情況還非常多,在每段方法的核心的代碼中,總是有一堆跟核心邏輯無關,卻又無法不寫的程式碼,有哪些?

真的非常多,諸如:

  • 權限檢查(permission)、
  • 參數檢查(Validation)、
  • 例外處理(Exception Handling)、
  • 日誌紀錄(Logging)

  • 等都是典型,些都是程式碼必須–但卻跟我們的Business Logic不全然直接相關–的邏輯。

這些,我們稱之為 “Infrastructure Logic”,在架構上屬於 cross-cutting concerns。

這類的程式碼,若能與Business Logic切割開來,將立即得到許多好處,諸如:

  • Infrastructure Logic和Business Logic耦合性降低,兩者分開後,各別將更容易重用。
  • 程式碼維護變的更加容易,開發時也可以更專注在Business Logic,不用分心去管雜七雜八的其他程式碼。
  • 程式碼可讀性也變高,閱讀時不會被不相關的code干擾…

總之,這麼做好處多多。但由於您可能還沒有辦法理解我們待會要怎麼分割兩者,因此你現在可能完全沒感覺,不急,繼續看下去就會慢慢理解。

而AOP希望實現的,就是讓這些Business Logic(BMI計算)和Infrastructure Logic(Log, 錯誤處理, …etc.)之間可以不要彼此過分倚賴…

好,那如何實現呢?
有很多種作法,我自己最喜歡的,是透過Attribute。

想像一下,倘若我們可以這樣做…
(注意底下的 Calculate()掛上了Attribute):

//👉當發生錯誤的時候發通知給 Someone
[ExceptionNotify(LineTo = "Someone")]
//👉這函式被呼叫,就寫log到檔案中
[Logging(LoggingTo = File)]
//計算BMI
public Decimal Calculate()
{
    Decimal result = 0;
     
    Decimal height = (Decimal)Height / 100;
    result = Weight / (height * height);

    return result;
}

倘若我們可以透過對Calculate()這個Method掛上我們寫好的Attribute,就賦予Calculate()這個Method特殊的功能,那就可以實現了。例如,掛上Attribute後,一旦發生exception,就自動發LINE訊息通知Admin,如果該Method被呼叫,就自動在Log file中記錄…

倘若這一切只需要掛上Attribute就能實現,我們就毋須破壞原本計算BMI的Calculate()這個method中的程式碼了…

但,真的可以這樣嗎?可以。

以Attribute實現AOP

請在專案中透過NuGet加入一個套件 isRock.Core.AOP,完成後,請先引用using isRock.Core.AOP ,接著設計一個Logging Attribute(請注意繼承自PolicyInjectionAttributeBase):

public class Logging : PolicyInjectionAttributeBase
{
    //指定Log File Name
    public string LogFileName { get; set; }
    //override AfterInvoke方法
    public override void AfterInvoke(object sender, PolicyInjectionAttributeEventArgs e)
    {
        var msg = $"\r\n Method '{((MethodInfo)sender).Name}' has been called - {DateTime.Now.ToString()} ";
        SaveLog(msg);
    }
    //寫入Log
    private void SaveLog(string msg)
    {
        if (System.IO.File.Exists(LogFileName))
        {
            System.IO.File.AppendAllText(LogFileName, msg);
        }
        else
        {
            System.IO.File.WriteAllText(LogFileName, msg);
        }
    }
}    

我們在上面這個自行設計的Attribute當中,override了AfterInvoke()這個方法,並在其中呼叫了SaveLog(),透過SaveLog()這個方法來儲存Log。

上述動作完成後,再將原本計算 BMI的Class修改如下(請注意只改了10, 22行, 並且為BMIProcessor 抽出了一個 IBMIProcessor 介面(1-8行):

public interface IBMIProcessor
{
    decimal BMI { get; }
    int Height { get; set; }
    int Weight { get; set; }

    decimal Calculate();
}

public class BMIProcessor : IBMIProcessor
{
    public int Weight { get; set; }
    public int Height { get; set; }
    public Decimal BMI
    {
        get
        {
            return Calculate();
        }
    }

    [Logging(LogFileName ="log.txt")]
    //計算BMI
    public Decimal Calculate()
    {
        Decimal result = 0;
        Decimal height = (Decimal)Height / 100;
        result = Weight / (height * height);

        return result;
    }
}

最後,再調整一下Main中的程式碼(只改了第4行)。

static void Main(string[] args)
{
    //建立BMIProcessor物件
    IBMIProcessor BMI = PolicyInjection.Create<IBMIProcessor>(new BMIProcessor());
    BMI.Height = 170;
    BMI.Weight = 70;
    //計算BMI
    var ret = BMI.Calculate();
    //顯示
    Console.WriteLine($"BMI : {ret}");

    Console.WriteLine("Press any key for continuing...");
    Console.ReadKey();
}

神奇的事情發生了。

你會發現,我們的主程式,上面的main()中其實只改了小小一行(第4行),就讓我們自己寫的Logging Attribute作用到Calculate()的身上了。由於Logging Attribute中的第6行override了AfterInvoke()這個Method,該Method會在Calculate()方法被呼叫後自動被執行,其中的SaveLog()程式碼是開檔寫Log…

如此一來,每當Calculate()方法被呼叫,log.txt檔案就會被開啟並被加入一列,變成:
圖片

這樣就實現了我們一開始的需求👉👉當某一個Method被掛上Attribute之後,就能夠增加原本沒有的功能。且將Business Logic和Infrastructure Logic做一個切割,達成cross-cutting concerns的獨立性。

我們回憶一下我們做了什麼?

  1. 首先我們引入了NuGet套件 isRock.Core.AOP
  2. Using了 isRock.Core.AOP
  3. 接著設計了一個自己的Logging Attribute(繼承自PolicyInjectionAttributeBase)
  4. 在上述Logging Attribute當中,override了AfterInvoke()方法,該方法會在目標Method(被掛上Attribute的Method)被叫用之後自動被呼叫
  5. 在上述AfterInvoke()方法中呼叫SaveLog()開檔並加上『目標Method被呼叫』的Log
  6. 為了讓Attribute生效,我們修改原始的BMIProcessor類別,在Calculate()方法上掛上LoggingAttribute:
[Logging(LogFileName ="log.txt")]
  1. 為BMIProcessor類別抽出介面IBMIProcessor
  2. 調整主程式為 IBMIProcessor BMI = PolicyInjection.Create(new BMIProcessor());

就這樣,我們透過這個框架,可以讓開發人員輕鬆的設計自己的Attribute,並且將Attribute掛在Method上,實現Business Logic和Infrastructure Logic分離的AOP情境。

骨子裡,他還是使用PolicyInjection的手法,透過reflection與Proxy的手法,完成以Attribute實現類似AOP的功能。

如果你深入看,會發現我們的PolicyInjectionAttributeBase已經實做了BeforeInvoke、AfterInvoke、OnException三種Method,足夠讓開發人員輕易的實現自己想實現的Attribute。

嚴格說起來,我們用PolicyInjection這樣的手法,雖然可以實現部份AOP的目標,儘管還夠不全面,但是,即便只是能實現到目前這樣,也已經很符合我們在日常開發工作當中使用了。

未來有機會,我們再來介紹剛才使用的 AOP 框架(套件)是如何開發的…


相關教育訓練: http://www.studyhost.tw/NewCourses/Architecture
若這篇文章對您有所幫助,請點選這裡加入FaceBook專頁按讚並追蹤,也歡迎您幫我們分享出去,謝謝您的支持。

留言

這個網誌中的熱門文章

使用LM Studio輕鬆在本地端以API呼叫大語言模型(LLM)

VS Code的字體大小

使用 Dify 建立企業請假機器人

使用C#開發LineBot(3) - 使用LineBotSDK發送Line訊息

使用Qdrant向量資料庫實作語意相似度比對