1. 前言
在學(xué)習(xí)nest.js的時(shí)候钾虐,我們都知道,這個(gè)框架采用的angular的思想梗搅,依賴注入禾唁,對(duì)于前端來說效览,這有點(diǎn)后端概念无切,所以我們現(xiàn)在來學(xué)習(xí)認(rèn)識(shí)這些設(shè)計(jì)模式。
2. 什么是依賴注入
2.1 它是設(shè)計(jì)模式
首先丐枉,依賴注入是一個(gè)設(shè)計(jì)模式哆键,因?yàn)樗鉀Q的是一類問題。這類問題是什么呢瘦锹?這類問題和依賴有關(guān)系籍嘹。
2.2 依賴倒轉(zhuǎn)原則
依賴倒轉(zhuǎn)原則(Dependence Inversion Priciple, DIP)提倡:
- 高層模塊不應(yīng)該依賴低層模塊。兩個(gè)都應(yīng)該依賴抽象
- 抽象不應(yīng)該依賴細(xì)節(jié)弯院,細(xì)節(jié)應(yīng)該依賴抽象
- 針對(duì)接口編程辱士,不要針對(duì)實(shí)現(xiàn)編程
要想講明白這個(gè)設(shè)計(jì)模式得先給大家說一個(gè)現(xiàn)實(shí)場(chǎng)景的案例:主板和內(nèi)存條 大家都知道內(nèi)存條依賴主板,內(nèi)存條壞了和主板無關(guān)听绳,主板壞了也和內(nèi)存條無關(guān)颂碘,可以把電腦理解成是大的軟件系統(tǒng),任何部件相互依賴椅挣,但又彼此獨(dú)立头岔,即這些部件就是電腦中封裝的類或程序集塔拳,在電腦里這叫易插拔,在編程中這叫強(qiáng)內(nèi)聚低耦合峡竣。 內(nèi)存模塊(高層模塊)不依賴主板模塊(低層模塊)靠抑,它們依賴的是被抽象的接口(模塊依賴都應(yīng)該依賴抽象),抽象不應(yīng)該依賴細(xì)節(jié)适掰,細(xì)節(jié)應(yīng)該依賴抽象這句話說白了颂碧,就是要針對(duì)借口編程,不要對(duì)實(shí)現(xiàn)編程攻谁。無論主板稚伍、cpu、內(nèi)存都是針對(duì)接口設(shè)計(jì)的戚宦,都是標(biāo)準(zhǔn)的接口个曙,如果針對(duì)實(shí)現(xiàn)來設(shè)計(jì),內(nèi)存要對(duì)應(yīng)到具體的廠商主板受楼,內(nèi)存壞了主板也得換垦搬。
這也就是說:在編程時(shí),我們對(duì)系統(tǒng)進(jìn)行模塊化艳汽,它們之間有依賴猴贰,比如模塊A依賴模塊B 那么依據(jù)依賴倒轉(zhuǎn)原則,模塊A應(yīng)該依賴模塊B的接口河狐,而不應(yīng)該依賴模塊B的實(shí)現(xiàn)米绕。
雖然模塊A只依賴接口編程,但在運(yùn)行的時(shí)候馋艺,它還是需要有一個(gè)具體的模塊來負(fù)責(zé)模塊A需要的功能的栅干,所以模塊A在【運(yùn)行時(shí)】是需要一個(gè)【真的】模塊B,而不是它的接口捐祠。即模塊A在【運(yùn)行時(shí)】需要有一個(gè)接口的實(shí)現(xiàn)模塊作為它的屬性碱鳞。 那么這個(gè)實(shí)現(xiàn)模塊怎么來?它是怎么初始化踱蛀,然后怎么傳給模塊A的窿给? 解決這個(gè)問題的就是依賴注入。
2.3 前端的依賴注入
依賴注入更多的是后端的概念率拒,對(duì)于前端來說崩泡,很少有抽象,更別說有接口了猬膨。但是角撞,依賴注入?yún)s是一直都存在,只是許多人沒有認(rèn)出來而已。
比如用過vue的都應(yīng)該知道m(xù)ain.js靴寂,其實(shí)他就是一個(gè)依賴注入磷蜀,我們見過有這樣一段代碼。
import ElementUI from 'element-ui' // vue的ui組件-(餓了么-ui)element-ui
Vue.use(ElementUI)
其實(shí)就是我們的需要的模塊依賴vue模塊百炬,main.js就是vue模塊抽象出來的接口褐隆,這里使用Vue.use(),把我們需要的模塊vue注入進(jìn)來剖踊,然后我們就可以用它了庶弃。 這是個(gè)很普通的代碼,太正常了德澈,我們每天都會(huì)寫這些代碼歇攻。 其實(shí)依賴注入它只做兩件事:
- 初始化被依賴的模塊
- 注入到依賴模塊中
這個(gè)時(shí)候應(yīng)該知道了,import ElementUI from 'element-ui'梆造,初始化了被依賴的模塊缴守;而Vue.use(ElementUI)是把我們依賴的模塊注入到依賴模塊中。 我們不依賴element-ui的具體實(shí)現(xiàn)镇辉,我們只是使用他這個(gè)庫的抽象接口而已屡穗,比如它的button組件。
2.4 依賴注入的作用
看了上面我們常用的引入是依賴注入忽肛,是不是有點(diǎn)吃驚村砂?不用覺得這是什么很屌的是,關(guān)于這個(gè)我們仔細(xì)深究她的作用:
初始化被依賴的模塊 注入到依賴模塊中
我們?yōu)槭裁葱枰蕾囎⑷肽兀?1. 初始化被依賴的模塊
如果不通過依賴注入模式來初始化被依賴的模塊屹逛,那么就要依賴模塊自己去初始化了 那么問題來了:依賴模塊就耦合了被依賴模塊的初始化信息了 2. 注入到依賴模塊中
被依賴模塊已經(jīng)被其他管理器初始化了础废,那么依賴模塊要怎么獲取這個(gè)模塊呢? 有兩種方式:
- 自己去問
- 別人主動(dòng)給你
沒用依賴注入模式的話是1罕模,用了之后就是2 想想评腺,你需要某個(gè)東西的時(shí)候,你去找別人要手销,你需要提供別人什么信息歇僧?最簡(jiǎn)單的就是那個(gè)東西叫什么图张,即你需要提供一個(gè)名稱锋拖。 所以,方式1的問題是:依賴模塊耦合了被依賴模塊的【名稱】還有那個(gè)【別人】 而方式2解決了這個(gè)問題祸轮,讓依賴模塊只依賴需要的模塊的接口兽埃。
3. 前端代碼實(shí)例
- 我們首先得為模塊依賴提供抽象的接口
- 下來應(yīng)該能夠注冊(cè)依賴關(guān)系
- 在注冊(cè)這個(gè)依賴關(guān)系后有地方存儲(chǔ)它
- 存儲(chǔ)后,我們應(yīng)該把被依賴的模塊注入依賴模塊中
- 注入應(yīng)該保持被傳遞函數(shù)的作用域
- 被傳遞的函數(shù)應(yīng)該能夠接受自定義參數(shù)适袜,而不僅僅是依賴描述
基于Injector柄错、dependencies和函數(shù)參數(shù)名的依賴注入
設(shè)想我們有1個(gè)模塊Student,它依賴3個(gè)模塊Notebook、Pencil售貌、School
function Notebook() {}
Notebook.prototype.notebookName = function () {
return 'this is a notebook'
}
function Pencil() {}
Pencil.prototype.printName = function () {
return 'this is a pencil'
}
function School() {}
School.prototype.schoolName = function () {
return '廣工大'
}
function Student() {}
Student.prototype.write = function (notebook, pencil, school) {
if (!notebook || !pencil || !school) {
throw new Error('Dependencies not provided!')
}
console.log('writing...')
console.log(notebook)
console.log(pencil)
console.log(school)
return '我擁有School给猾、Pencil和Notebook'
}
我們需要在Student中用到,它依賴的3個(gè)模塊Notebook颂跨、Pencil敢伸、School,記得依賴注入的功能:初始化被依賴的模塊恒削,注入到依賴模塊中池颈。
知道這些根據(jù)我們的目標(biāo),下面開始我們的injector接口钓丰,第一種方法:從方法中解析出參數(shù)的依賴注入
var injector = { // 依賴注入的抽象接口
dependencies: {}, // 存儲(chǔ)被依賴的模塊
register: function (key, value) { // 注冊(cè)初始化被依賴的模塊
this.dependencies[key] = value
},
resolve: function (deps, func, scope) { // 注入到依賴的模塊中躯砰,注入應(yīng)該接受一個(gè)函數(shù),并返回一個(gè)我們需要的函數(shù)
var paramNames = this.getParamNames(func) // 取得參數(shù)名
var params = []
for (var i = 0; i < paramNames.length; i++) { // 通過參數(shù)名在dependencies中取出相應(yīng)的依賴
let d = paramNames[i]
let depen = this.dependencies[d] || deps[i]
if (depen) {
params.push(depen)
} else {
throw new Error('缺失的依賴:' + d)
}
}
// 注入依賴,執(zhí)行,并返回一個(gè)我們需要的函數(shù)
return func.apply(scope || {}, params) // 將func作用域中的this關(guān)鍵字綁定到bind對(duì)象上携丁,bind對(duì)象可以為空
},
getParamNames: function (func) { // 獲取方法的參數(shù)名字
var paramNames = func.toString().match(/^function\s*[^\(]*\(\s*([^\)]*)\)/m)[1]
paramNames = paramNames.replace(/ /g, '')
paramNames = paramNames.split(',')
return paramNames // Array
}
}
這里我們使用register方法來注冊(cè)初始化被依賴的模塊琢歇,使用dependencies來存儲(chǔ)被依賴的模塊,resolve來進(jìn)行注入梦鉴,getParamNames來獲取我們的傳遞方法的參數(shù)矿微,注入依賴,執(zhí)行尚揣,并返回一個(gè)我們需要的函數(shù)涌矢。
調(diào)用代碼:
injector.register('notebook', new Notebook()) // 注冊(cè)notebook
injector.register('pencil', new Pencil()) // 注冊(cè)pencil
var school = new School()
var student = new Student()
// 以參數(shù)的形式傳入school
var studentWrite = injector.resolve([, , school], student.write, student)
console.log(studentWrite) // "我擁有School、Pencil和Notebook"
執(zhí)行結(jié)果:
上面我們通過injector(注入器快骗、注射器)向write方法提供它所需要的依賴娜庇。通過依賴注入,函數(shù)的執(zhí)行和其所依賴對(duì)象的創(chuàng)建邏輯就被解耦開來了方篮。這里我們初始化的方式有兩種:一種是通過injector.register
來注冊(cè)名秀,一種是直接使用injector.resolve
中的deps來注冊(cè)其依賴的。
這種方法我們是從方法中解析出參數(shù)她的參數(shù)得知他所依賴的模塊藕溅。下面來說說另外一種方法:從方法中解析出參數(shù)及參數(shù)聲明的依賴注入匕得。
var injector = { // 依賴注入的抽象接口
dependencies: {}, // 存儲(chǔ)被依賴的模塊
isDeclare: false, // 是否為參數(shù)聲明
param: [], // 聲明參數(shù)存儲(chǔ)
paramDeclare: function (param) { // 依賴注入?yún)?shù)聲明
if (Object.prototype.toString.call(param) !== '[object Array]') {
try {
throw new Error('接受的是一個(gè)數(shù)組,但是卻得到一個(gè)' + typeof filterArray)
} catch (e) {
console.error(e)
}
}
this.param = param.concat()
console.log(this.param)
this.isDeclare = true
},
register: function (key, value) { // 注冊(cè)初始化被依賴的模塊
this.dependencies[key] = value
},
resolve: function (deps, func, scope) { // 注入到依賴的模塊中,注入應(yīng)該接受一個(gè)函數(shù)巾表,并返回一個(gè)我們需要的函數(shù)
console.log(deps)
var paramNames = this.isDeclare ? this.param : this.getParamNames(func) // 取得參數(shù)名
if (paramNames.length === 0 && this.isDeclare) {
throw new Error('缺失的參數(shù)聲明')
}
if (paramNames.length === 0 && !this.isDeclare) {
throw new Error('該方法沒有參數(shù)依賴')
}
var params = []
for (var i = 0; i < paramNames.length; i++) { // 通過參數(shù)名在dependencies中取出相應(yīng)的依賴
let d = paramNames[i]
let depen = this.dependencies[d] || deps[i]
if (depen) {
params.push(depen)
} else {
throw new Error('缺失的依賴:' + d)
}
}
// 注入依賴,執(zhí)行,并返回一個(gè)我們需要的函數(shù)
return func.apply(scope || {}, params) // 將func作用域中的this關(guān)鍵字綁定到bind對(duì)象上汁掠,bind對(duì)象可以為空
},
getParamNames: function (func) { // 獲取方法的參數(shù)名字
var paramNames = func.toString().match(/^function\s*[^\(]*\(\s*([^\)]*)\)/m)[1]
paramNames = paramNames.replace(/ /g, '')
paramNames = paramNames.split(',')
return paramNames // Array
}
}
調(diào)用代碼1:
injector.register('notebook', new Notebook()) // 注冊(cè)notebook
injector.register('pencil', new Pencil()) // 注冊(cè)pencil
var school = new School()
var student = new Student()
// 以參數(shù)的形式傳入school
var studentWrite = injector.resolve([, , school], student.write, student)
console.log(studentWrite) // "我擁有School、Pencil和Notebook"
調(diào)用代碼2:
injector.register('notebook', new Notebook())
injector.register('pencil', new Pencil())
injector.paramDeclare(['notebook', 'pencil', 'school']) // injector.paramDeclare-依賴注入?yún)?shù)聲明
var school = new School()
var student = new Student()
var studentWrite = injector.resolve([, , school], student.write, student)
console.log(studentWrite) // "我擁有School集币、Pencil和Notebook"
4. 什么是IoC
Ioc - Inversion of Control , 即"控制反轉(zhuǎn)"考阱。在開發(fā)中, IoC 意味著你設(shè)計(jì)好的對(duì)象交給容器控制鞠苟,而不是使用傳統(tǒng)的方式乞榨,在對(duì)象內(nèi)部直接控制秽之。
如何理解好 IoC 呢?理解好 IoC的關(guān)鍵是要明確"誰控制誰吃既,控制什么考榨,為何是反轉(zhuǎn)(有反轉(zhuǎn)就應(yīng)該有正轉(zhuǎn)),哪些方面反轉(zhuǎn)了"鹦倚,我們來深入分析一下董虱。
誰控制誰,控制什么: 在傳統(tǒng)的程序設(shè)計(jì)中申鱼,我們直接在對(duì)象內(nèi)部通過 new 的方式創(chuàng)建對(duì)象愤诱,是程序主動(dòng)創(chuàng)建依賴對(duì)象;而 IoC 是有專門一個(gè)容器來創(chuàng)建這些對(duì)象捐友,即由 IoC 容器控制對(duì)象的創(chuàng)建淫半;誰控制誰?當(dāng)然是 IoC 容器控制了對(duì)象匣砖;控制什么科吭?主要是控制外部資源獲取。
為何是反轉(zhuǎn)了猴鲫,哪些方面反轉(zhuǎn)了: 有反轉(zhuǎn)就有正轉(zhuǎn)对人,傳統(tǒng)應(yīng)用程序是由我們自己在對(duì)象中主動(dòng)控制去獲取依賴對(duì)象,也就是正轉(zhuǎn)拂共;而反轉(zhuǎn)則是由容器來幫忙創(chuàng)建及注入依賴對(duì)象牺弄;為何是反轉(zhuǎn)?因?yàn)橛扇萜鲙臀覀儾檎壹白⑷胍蕾噷?duì)象宜狐,對(duì)象只是被動(dòng)的接受依賴對(duì)象势告,所以是反轉(zhuǎn)了;哪些方面反轉(zhuǎn)了抚恒?依賴對(duì)象的獲取被反轉(zhuǎn)了咱台。
5. IoC能做什么
Ioc 不是一種技術(shù),只是一種思想俭驮,一個(gè)重要的面向?qū)ο缶幊谭▌t回溺,它能指導(dǎo)我們?nèi)绾卧O(shè)計(jì)松耦合、更優(yōu)良的系統(tǒng)混萝。傳統(tǒng)應(yīng)用程序都是由我們?cè)陬悆?nèi)部主動(dòng)創(chuàng)建依賴對(duì)象遗遵,從而導(dǎo)致類與類之間高耦合,難于測(cè)試譬圣;有了 IoC 容器后瓮恭,把創(chuàng)建和查找依賴對(duì)象的控制權(quán)交給了容器雄坪,由容器注入組合對(duì)象厘熟,所以對(duì)象之間是松散耦合屯蹦,這樣也便于測(cè)試,利于功能復(fù)用绳姨,更重要的是使得程序的整個(gè)體系結(jié)構(gòu)變得非常靈活登澜。
其實(shí) IoC 對(duì)編程帶來的最大改變不是從代碼上,而是思想上飘庄,發(fā)生了"主從換位"的變化脑蠕。應(yīng)用程序本來是老大,要獲取什么資源都是主動(dòng)出擊跪削,但在 IoC思想中谴仙,應(yīng)用程序就變成被動(dòng)了,被動(dòng)的等待 IoC 容器來創(chuàng)建并注入它所需的資源了碾盐。
6. IoC 和 DI
DI - Dependency Injection晃跺,即"依賴注入":組件之間的依賴關(guān)系由容器在運(yùn)行期決定,形象的說毫玖,即由容器動(dòng)態(tài)的將某個(gè)依賴關(guān)系注入到組件之中掀虎。依賴注入的目的并非為軟件系統(tǒng)帶來更多功能,而是為了提升組件重用的頻率付枫,并為系統(tǒng)搭建一個(gè)靈活烹玉、可擴(kuò)展的平臺(tái)。通過依賴注入機(jī)制阐滩,我們只需要通過簡(jiǎn)單的配置二打,而無需任何代碼就可指定目標(biāo)需要的資源,完成自身的業(yè)務(wù)邏輯掂榔,而不需要關(guān)心具體的資源來自何處址儒,由誰實(shí)現(xiàn)。
理解 DI 的關(guān)鍵是:"誰依賴了誰衅疙,為什么需要依賴莲趣,誰注入了誰,注入了什么"饱溢,那我們來深入分析一下:
誰依賴了誰:當(dāng)然是應(yīng)用程序依賴 IoC 容器
為什么需要依賴:應(yīng)用程序需要 IoC 容器來提供對(duì)象需要的外部資源
誰注入誰:很明顯是 IoC 容器注入應(yīng)用程序依賴的對(duì)象
注入了什么:注入某個(gè)對(duì)象所需的外部資源(包括對(duì)象喧伞、資源、常量數(shù)據(jù))
IoC 和 DI 有什么關(guān)系绩郎?其實(shí)它們是同一個(gè)概念的不同角度描述潘鲫,由于控制反轉(zhuǎn)的概念比較含糊(可能只是理解為容器控制對(duì)象這一個(gè)層面,很難讓人想到誰來維護(hù)依賴關(guān)系)肋杖,所以 2004 年大師級(jí)人物 Martin Fowler 又給出了一個(gè)新的名字:"依賴注入"溉仑,相對(duì) IoC 而言,"依賴注入" 明確描述了被注入對(duì)象依賴 IoC 容器配置依賴對(duì)象状植。
總的來說浊竟, 控制反轉(zhuǎn)(Inversion of Control)是說創(chuàng)建對(duì)象的控制權(quán)發(fā)生轉(zhuǎn)移怨喘,以前創(chuàng)建對(duì)象的主動(dòng)權(quán)和創(chuàng)建時(shí)機(jī)由應(yīng)用程序把控,而現(xiàn)在這種權(quán)利轉(zhuǎn)交給 IoC 容器振定,它就是一個(gè)專門用來創(chuàng)建對(duì)象的工廠必怜,你需要什么對(duì)象,它就給你什么對(duì)象后频。有了 IoC 容器梳庆,依賴關(guān)系就改變了,原先的依賴關(guān)系就沒了卑惜,它們都依賴 IoC容器了膏执,通過 IoC 容器來建立它們之間的關(guān)系。
控制反轉(zhuǎn):創(chuàng)建對(duì)象實(shí)例的控制權(quán)從代碼控制剝離到IOC容器控制露久,實(shí)際就是你在xml文件控制胧后,側(cè)重于原理。 依賴注入:創(chuàng)建對(duì)象實(shí)例時(shí)抱环,為這個(gè)對(duì)象注入屬性值或其它對(duì)象實(shí)例壳快,側(cè)重于實(shí)現(xiàn)。
依賴注入和控制反轉(zhuǎn)是同一概念镇草,是對(duì)同一件事情的不同描述眶痰,它們描述的角度不同。
依賴注入是從應(yīng)用程序的角度在描述:應(yīng)用程序依賴容器創(chuàng)建并注入它所需要的外部資源梯啤;
而控制反轉(zhuǎn)是從容器的角度在描述:容器控制應(yīng)用程序竖伯,由容器反向的向應(yīng)用程序注入應(yīng)用程序所需要的外部資源(對(duì)象、文件等)因宇。
相關(guān)文章: