AOP和IoC在點(diǎn)我達(dá)前端的實(shí)踐

本文同步發(fā)表在豆米的博客:豆米的博客

1、前言

如今的編程模型有很多種,常用的是面向過程編程(POP)、面向?qū)ο缶幊?OOP)。其實(shí)還有好幾種編程模型:面向切面編程(AOP,也就是我們今天要討論的主題)、響應(yīng)式編程、函數(shù)式編程。每種編程模型都有其對應(yīng)的應(yīng)用場景,今天我們只討論AOP(順帶捎上IoC),其他幾種后面有時(shí)間可以拿出來學(xué)習(xí)。

2、AOP和IoC的介紹

2.1、AOP簡述

AOP(Aspect Oriented Programming)主要實(shí)現(xiàn)的目的是針對業(yè)務(wù)處理過程中的切面進(jìn)行提取,它所面對的是處理過程中的某個(gè)步驟或階段,以獲得邏輯過程中各部分之間低耦合性的隔離效果。它是對傳統(tǒng)OOP編程的一種補(bǔ)充。

OOP是關(guān)注將需求功能劃分為不同的并且相對獨(dú)立,封裝良好的類,并讓它們有著屬于自己的行為,依靠繼承和多態(tài)等來定義彼此的關(guān)系;AOP是希望能夠?qū)⑼ㄓ眯枨蠊δ軓牟幌嚓P(guān)的類當(dāng)中分離出來,能夠使得很多類共享一個(gè)行為,一旦發(fā)生變化,不必修改很多類,而只需要修改這個(gè)行為即可。

2.2、IoC簡述

IoC(Inversion of control)控制反轉(zhuǎn),它不是一種技術(shù),只是一種思想,一個(gè)重要的面向?qū)ο缶幊痰姆▌t,它能指導(dǎo)我們?nèi)绾卧O(shè)計(jì)出松耦合、更優(yōu)良的程序。傳統(tǒng)應(yīng)用程序都是由我們在類內(nèi)部主動(dòng)創(chuàng)建依賴對象,從而導(dǎo)致類與類之間高耦合,難于測試;有了IoC容器后,把創(chuàng)建和查找依賴對象的控制權(quán)交給了容器,由容器進(jìn)行注入組合對象,所以對象與對象之間是 松散耦合,這樣也方便測試,利于功能復(fù)用,更重要的是使得程序的整個(gè)體系結(jié)構(gòu)變得非常靈活。

談到IoC就不得不說DI(dependency injection)依賴注入,這個(gè)概念是大師級人物Martin Fowler在2004年提出的,只是為了更明確地描述IoC(IoC的另外一種實(shí)現(xiàn)方式叫做)。所以二者本質(zhì)是一樣的。依賴注入的目的并非為軟件系統(tǒng)帶來更多功能,而是為了提升組件重用的頻率,并為系統(tǒng)搭建一個(gè)靈活、可擴(kuò)展的平臺(tái)。通過依賴注入機(jī)制,我們只需要通過簡單的配置,而無需任何代碼就可指定目標(biāo)需要的資源,完成自身的業(yè)務(wù)邏輯,而不需要關(guān)心具體的資源來自何處,由誰實(shí)現(xiàn)。

2.3、 AOP想要解決的痛點(diǎn)

假設(shè)有這么一個(gè)計(jì)算器類(Typescript):

interface Calculator {
  add: (number num1, number num2) => number
  sub: (number num1, number num2) => number
  div: (number num1, number num2) => number
  mul: (number num1, number num2) => number
}

class Cal implements Calculator {
  constructor() {}
  add(number num1, number num2) {
    return num1 + num2
  }
  sub(number num1, number num2) {
    return num1 - num2
  }
  div(number num1, number num2) {
    return num1 / num2
  }
  mul(number num1, number num2) {
    return num1 * num2
  }
}

然后你想要在每一個(gè)計(jì)算方法中添加追蹤日志,于是改造成:

interface Calculator {
  add: (number num1, number num2) => number
  sub: (number num1, number num2) => number
  div: (number num1, number num2) => number
  mul: (number num1, number num2) => number
}

class Cal implements Calculator {
  constructor() {}
  add(number num1, number num2) {
    console.log(`add method calling, params: num1[${num1}],num2[${num2}]`)
    const result = num1 + num2
    console.log(`add method ending, result: [${result}]`)
    return result
  }
  sub(number num1, number num2) {
    console.log(`sub method calling, params: num1[${num1}],num2[${num2}]`)
    const result = num1 - num2
    console.log(`sub method ending, result: [${result}]`)
    return result
  }
  div(number num1, number num2) {
    console.log(`div method calling, params: num1[${num1}],num2[${num2}]`)
    const result = num1 / num2
    console.log(`div method ending, result: [${result}]`)
    return result
  }
  mul(number num1, number num2) {
    console.log(`mul method calling, params: num1[${num1}],num2[${num2}]`)
    const result = num1 * num2
    console.log(`mul method ending, result: [${result}]`)
    return result
  }
}

然后你可能還需要添加參數(shù)校驗(yàn),于是又有各種校驗(yàn)邏輯加入,并且這種非業(yè)務(wù)的需求還會(huì)不斷增加,于是就會(huì)不斷添加重復(fù)代碼,對于一個(gè)開發(fā)人員來說,新做一個(gè)需求就會(huì)不斷地copy-paste。然后哪一天你需要改動(dòng)日志顯示方式,很可能就需要改所有的地方,一旦某處漏改,就造成顯示不一致。由此可見這種代碼是很難維護(hù)的。

那么針對這些痛點(diǎn),AOP提出了橫切關(guān)注點(diǎn)(crosscutting concern)的概念,這些與業(yè)務(wù)無關(guān)但是屬于系統(tǒng)范圍的需求并且會(huì)橫跨多個(gè)模塊的功能稱為橫切關(guān)注點(diǎn)。使用AOP編程模型,我們可以簡化出這樣的一張圖:

image

看這個(gè)圖是不是很像expressjs的中間件的角色?

2.4、IoC想要解決的痛點(diǎn)

在實(shí)際網(wǎng)關(guān)開發(fā)中,我們會(huì)創(chuàng)建很多service,諸如redis、disconf、logger、cache之類的,然后我們這樣使用:

比如:

在controller1中用到redis: import redis from '...'

在controller2用到cache: import cache from '...'

大家都覺得這種寫法沒有什么不好,很符合我們的編程思維。但是這種寫法有一個(gè)很大的問題,那就是耦合性太高。如果某一天你需要擴(kuò)展redis service的實(shí)現(xiàn)方式,,比如需要增加一個(gè)參數(shù),告知redis service去連接一個(gè)性能更好的redis數(shù)據(jù)庫,這個(gè)時(shí)候所有引用到redis服務(wù)的都需要更改代碼。

舉個(gè)更具體的例子(typescript):

class Finder {
  find: (...) => {...}
}
class Fridge {
  finder:  Finder
  constructor() {
    this.finder = new Finder()
  }
  getApple() {
    return this.finder.find('apple')
  }
}

在上面的例子中我們看到創(chuàng)建一個(gè)冰箱類,提供一個(gè)查找蘋果的方法,但具體怎么查找是使用另外一個(gè)實(shí)例Finder的。這樣看起來一切都是ok的。但是如我們剛才說的,如果現(xiàn)在我們想要增加一個(gè)參數(shù),保證Finder類查找的東西肯定位于冷藏室呢?于是我們就需要改造類Finder,改造完類Finder還需要改造類Fridge,如果在系統(tǒng)中我們有很多類同時(shí)用到了這個(gè)Finder呢?那么就得改動(dòng)很多地方,萬一有漏改的呢?

結(jié)合上面的例子和實(shí)際應(yīng)用,我們的IoC便是要處理這樣的耦合。我們借助容器的概念,將類的創(chuàng)建和查找都放在容器中實(shí)現(xiàn),Fridge類不用關(guān)心Finder類的創(chuàng)建,只需要向容器要求使用Finder類,其他的一概不care,這樣就將實(shí)體類(concrete class)與抽象類解耦掉,改造之前如圖所示:

image

改造后:

image

3、 AOP在點(diǎn)我達(dá)網(wǎng)關(guān)的應(yīng)用

在點(diǎn)我達(dá)網(wǎng)關(guān)項(xiàng)目開發(fā)中,一條request完整的鏈路大致如下:

image

相信這個(gè)處理過程,在別的公司也都是成立的。所有的controller代碼除了業(yè)務(wù)邏輯不一樣外,剩余的全都是一個(gè)套路,都是一套重復(fù)代碼。起初網(wǎng)關(guān)的第一個(gè)框架版本就是這樣設(shè)計(jì)的,而引入AOP之后,我們得到的效果圖是這樣的:

image

相對比于上面的那張圖,這個(gè)改造可以看出我們將所有無關(guān)業(yè)務(wù)的需求作為切面抽出去,不再跟業(yè)務(wù)邏輯耦合,統(tǒng)一一個(gè)地方去維護(hù)代碼,讓整個(gè)網(wǎng)關(guān)的可維護(hù)性大大提高。代碼量的大量減少也讓開發(fā)人員可以更加專注于需求的開發(fā)。另外給整套網(wǎng)關(guān)帶來了很大的靈活性和擴(kuò)展性。

4、IoC在點(diǎn)我達(dá)網(wǎng)關(guān)的應(yīng)用

在點(diǎn)我達(dá)網(wǎng)關(guān)項(xiàng)目開發(fā),我們借助InversifyJS來實(shí)現(xiàn)IoC容器,實(shí)現(xiàn)的框架如下:

image

其他層次我們不必關(guān)注,有興趣可以私聊。在容器層中我們會(huì)在服務(wù)器啟動(dòng)的時(shí)候主動(dòng)創(chuàng)建多個(gè)實(shí)例,這些實(shí)例由容器維護(hù)并查找,當(dāng)我們在中間件中或者業(yè)務(wù)邏輯層中需要用到這些實(shí)例的時(shí)候,只需要這么使用即可:

@lazyInject(TYPE.Logger) private logger: winston.LoggerInstance

我們得controller類不會(huì)再去關(guān)注依賴類的初始化創(chuàng)建,而只管使用,就好比是你找女朋友,如果去婚介所的話,你是不用關(guān)心你想找的女孩子在哪里,而只需要到婚介所拿到女孩子的信息即可,而這個(gè)容器就類似于婚介所。

在這里就不過多地展開,有興趣的可以參考inversifyJs/Wiki

最后

點(diǎn)我達(dá)前端在Nodejs網(wǎng)關(guān)的實(shí)踐中積累了不少的經(jīng)驗(yàn),包括微服務(wù)化和異地多活改造,當(dāng)然也有今天說的這些概念。我們在整套前端工程化體系中同樣沉淀了一些符合大家需求的經(jīng)驗(yàn),包括組件庫、前端框架封裝、Nodejs網(wǎng)關(guān)框架設(shè)計(jì)、腳手架、網(wǎng)關(guān)運(yùn)維監(jiān)控、Mock服務(wù)器等等。后面有時(shí)間可以先分享一下微服務(wù)在點(diǎn)我達(dá)網(wǎng)關(guān)的實(shí)踐。同時(shí)也歡迎大家探討交流。

參考

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

相關(guān)閱讀更多精彩內(nèi)容

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