面向切面編程(AOP)初涉

面向切面編程(AOP)是 Aspect Oriented Programming 的縮寫(xiě),脫胎于函數(shù)式編程,是一種無(wú)侵入式的編程風(fēng)格。

無(wú)侵入式編程

所謂無(wú)侵入式編程,就是在不修改原有代碼的基礎(chǔ)上,對(duì)原始代碼進(jìn)行一些功能拓展。??梢詰?yīng)用于諸如代碼統(tǒng)計(jì)、日志打印、原始函數(shù)功能補(bǔ)充等場(chǎng)景中。
下面通過(guò)一些示例來(lái)展示這種編程風(fēng)格在 JavaScript 中的應(yīng)用。

舉個(gè)吃飯的例子

這里舉一個(gè)吃飯的例子,你正在做一個(gè)關(guān)于“吃”的項(xiàng)目,項(xiàng)目中你定義了一個(gè) eat 函數(shù):

function eat(){
    console.log("我正在吃糖醋排骨,好香好香")
}

隨后隨著需求升級(jí),PM 要求你完善整個(gè)吃飯的流程,一個(gè)比較完整的吃飯流程應(yīng)該是:做飯、吃飯、刷碗。此處你只定義了“吃”的方法,新需求還要求你實(shí)現(xiàn)“做飯”和“刷碗”兩項(xiàng)功能。
你可以直接對(duì) eat 函數(shù)進(jìn)行修改,在函數(shù)開(kāi)頭加入“做飯”的功能,在函數(shù)結(jié)尾加入“刷碗”的功能,但你知道這樣是不好的,因?yàn)檫@種方式侵入了原始的 eat 函數(shù),降低了代碼的可維護(hù)性,也增加了出錯(cuò)的可能性。
不讓修改原始的 eat 函數(shù)怎么辦?此時(shí)就需要一種無(wú)侵入式的編程范式,也就是本文要介紹的 AOP 編程。

JavaScript 中 AOP 的實(shí)現(xiàn)

在 JavaScript 中實(shí)現(xiàn) AOP 編程,主要是通過(guò) Function.prototype 完成。此例中,我們可以在 Function.prorotype 上掛載一個(gè) before 方法和 after 方法,分別在 eat 函數(shù)執(zhí)行前后調(diào)用。

Function.prototype.before = function(fn){
    fn && fn();
    this()
}

Function.prototype.after = function(fn){
    this()
    fn && fn();
}

此時(shí)如果我們調(diào)用 before 函數(shù):

eat.before(()=>{
    console.log("我先去做飯")
});

輸出結(jié)果為:

我先去做飯
我正在吃糖醋排骨,好香好香

同理,如果我們調(diào)用 after 函數(shù):

eat.after(()=>{
    console.log("我得去刷碗了")
});

輸出結(jié)果為:

我正在吃糖醋排骨,好香好香
我得去刷碗了

現(xiàn)在,我們已經(jīng)初步實(shí)現(xiàn)了一個(gè) AOP 的編程模型。

實(shí)現(xiàn)鏈?zhǔn)秸{(diào)用

上面的代碼有沒(méi)有什么問(wèn)題呢?在我看來(lái),至少有兩個(gè)問(wèn)題:

  • 同時(shí)調(diào)用 before 函數(shù)和 after 函數(shù),會(huì)導(dǎo)致 eat 函數(shù)被調(diào)用兩次
  • 無(wú)法實(shí)現(xiàn)鏈?zhǔn)秸{(diào)用

上面的代碼,如果只是單獨(dú)調(diào)用 beforeafter 函數(shù),沒(méi)有什么問(wèn)題。但新流程是需要我們同時(shí)調(diào)用 beforeafter 方法的:

eat.before(()=>{
    console.log("我先去做飯")
});

eat.after(()=>{
    console.log("我得去刷碗了")
});

下面是運(yùn)行結(jié)果:

我先去做飯
我正在吃糖醋排骨,好香好香
我正在吃糖醋排骨,好香好香
我得去刷碗了

可見(jiàn),eat 函數(shù)被調(diào)用了兩次,如果我們想在“做飯”之前在做點(diǎn)操作,比如“買(mǎi)菜”,會(huì)導(dǎo)致 eat 函數(shù)多次的調(diào)用。究其原因,是因?yàn)槲覀冋{(diào)用 beforeafter 函數(shù)時(shí),會(huì)立即調(diào)用 eat 函數(shù),我們可以對(duì) before 函數(shù)和 after 函數(shù)做一些修改,讓它們返回一個(gè)函數(shù):

Function.prototype.before = function(fn){
    return () => {
        fn && fn();
        this()
    }
}

Function.prototype.after = function(fn){
    return () => {
        this()
        fn && fn();
    }
}

通過(guò)返回一個(gè)函數(shù),實(shí)現(xiàn)了 eat 的延遲調(diào)用,同時(shí)由于返回值是一個(gè)函數(shù),我們還可以對(duì)該返回值應(yīng)用 beforeafter 函數(shù),并實(shí)現(xiàn)鏈?zhǔn)秸{(diào)用,這得益于 JavaScript 的靈活性。
修改代碼后,我們進(jìn)行以下調(diào)用:

eat.before(()=>{
    console.log("我先去做飯")
}).after(()=>{
    console.log("我得去刷碗了")
})();

你可以把 beforeafter 作為一個(gè)包裝器,類(lèi)似于 bind 方法,都是綁定在 Function .prototype 上的方法,對(duì)調(diào)用這些函數(shù)的函數(shù)(this)進(jìn)行一些包裝,返回一個(gè)新的函數(shù)數(shù),而原始的函數(shù)不受影響。
現(xiàn)在我們實(shí)現(xiàn)了一個(gè)具有一定擴(kuò)展性的 AOP 編程模型,此例中,如果你想在“做飯”之前加上“買(mǎi)菜”功能,在“刷碗”后加上“看電視”功能,以及其他任意的功能,都可以很方便的添加:

eat.before(()=>{
    console.log("我先去做飯")
}).after(()=>{
    console.log("我得去刷碗了")
}).before(()=>{
    console.log("我先去買(mǎi)菜")
}).after(()=>{
    console.log("我去休息一下")
})();

運(yùn)行結(jié)果:

我先去買(mǎi)菜
我先去做飯
我正在吃糖醋排骨,好香好香
我得去刷碗了
我去休息一下

函數(shù)返回值

經(jīng)過(guò)一連串的鏈?zhǔn)秸{(diào)用后,怎么獲取原始函數(shù)的返回值呢?如果我們的 eat 函數(shù)有個(gè)返回值:

function eat(){
    console.log("我正在吃糖醋排骨,好香好香");
    return {
        "dish":"糖醋排骨"
    }
}

要想獲取原始函數(shù)的返回值,我們?cè)賹?duì) beforeafter 做一些修改:

Function.prototype.before = function(fn){
    return () => {
        fn && fn();
        const result = this();
        return result;
    }
}

Function.prototype.after = function(fn){
    return () => {
        const result = this();
        fn && fn();
        return result;
    }
}

這里對(duì) beforeafter 函數(shù)返回的函數(shù)增加了返回值,返回值就是調(diào)用該函數(shù)的函數(shù)(這里是 eat)的返回值,這樣一來(lái),eat 函數(shù)的返回值就可以層層進(jìn)行傳遞下去了:

const retVal = eat.before(()=>{
    console.log("我先去做飯")
}).after(()=>{
    console.log("我得去刷碗了")
}).before(()=>{
    console.log("我先去買(mǎi)菜")
}).after(()=>{
    console.log("我去休息一下")
})();

console.log(retVal)

調(diào)用結(jié)果如下:

我先去買(mǎi)菜
我先去做飯
我正在吃糖醋排骨,好香好香
我得去刷碗了
我去休息一下
{ dish: '糖醋排骨' }

實(shí)例:代碼統(tǒng)計(jì)

通過(guò)前面的介紹,我們實(shí)現(xiàn)了一個(gè) AOP 的編程模型,盡管這個(gè)模型并不是盡善盡美的,但我們已經(jīng)可以用來(lái)做一些事情了。我們來(lái)做一個(gè)實(shí)現(xiàn)代碼統(tǒng)計(jì)的例子,通過(guò)代碼統(tǒng)計(jì),可以找出一些運(yùn)行時(shí)間較長(zhǎng)的函數(shù),然后對(duì)其進(jìn)行優(yōu)化。這里我們準(zhǔn)備對(duì) test 函數(shù)進(jìn)行代碼統(tǒng)計(jì):

function test(){
    for(let i = 0; i < 100000000;i++){}
}

我們可以實(shí)現(xiàn)一個(gè) getRunTime 方法,該方法用來(lái)進(jìn)行代碼統(tǒng)計(jì):

function getRunTime(fn){
    // 定義開(kāi)始時(shí)間和結(jié)束時(shí)間
    let start;
    let end;
    // 使用獲取代碼運(yùn)行時(shí)間
    fn.before(()=>{
        start = +new Date();
    }).after(()=>{
        end = +new Date();
    })();
    
    return end - start;
}

然后只需調(diào)用 getRunTime 方法,就可以實(shí)現(xiàn)代碼統(tǒng)計(jì)了:

const runTime = getRunTime(test)
console.log(runTime)

運(yùn)行結(jié)果:

66

getRunTime 的內(nèi)部實(shí)現(xiàn)是依賴(lài)于 AOP 的,通過(guò) AOP 實(shí)現(xiàn)了無(wú)侵入式的編程,我們?cè)跊](méi)有對(duì) test 內(nèi)部代碼進(jìn)行任何修改的情況下,實(shí)現(xiàn)了對(duì)其的運(yùn)行時(shí)長(zhǎng)進(jìn)行統(tǒng)計(jì),這種編程范式的優(yōu)點(diǎn),大家可以細(xì)細(xì)體會(huì)。

總結(jié)

本文對(duì)面向切面編程(AOP)的概念進(jìn)行了一些介紹,并給出了 JavaScript 的實(shí)現(xiàn)方式,最后給出了一個(gè)例子。
在 JavaScript 中實(shí)現(xiàn) AOP 的原理很簡(jiǎn)單,就是通過(guò)對(duì) Function.prototype 進(jìn)行擴(kuò)展,這種方式也應(yīng)用于諸多內(nèi)置函數(shù)中,如常用的 bind 函數(shù),我們應(yīng)該主要學(xué)習(xí)這種思想。
在 Python 語(yǔ)言中,裝飾器也是一種面向切面編程的范式,我在這篇文章中也進(jìn)行了一些介紹,大家可以對(duì)比學(xué)習(xí)。

完。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容