當你手上只有錘子的時候箭窜,看什么都像釘子。
引言
設(shè)計模式往往被用來檢驗一個程序員是否高端,因為從道理上來講婆咸,
在很多地方即使你寫的代碼和意大利面一樣竹捉,它只要能實現(xiàn)功能。你通常就不會被老板找麻煩尚骄。
然而只要公司上了一定的規(guī)模块差,有了編程規(guī)范,甚至復(fù)查倔丈,你要再寫的和意大利面一樣憨闰,可能很快你就吃不起意大利面了。
雖然需五,通常如果你只會寫意大利面一般的代碼鹉动,你也進不去這樣的地方。
扯遠了宏邮,我只是想說明泽示,當程序有了一定的規(guī)模,編程范式就會顯示出它的作用蜜氨,無論是從編寫還是維護兩方面來說械筛。
那么什么是設(shè)計模式呢?
設(shè)計模式的定義為:在面向?qū)ο筌浖O(shè)計過程中记劝,針對特定問題的簡潔而優(yōu)雅的解決方案变姨。
說人話就是,特定場景的某種問題的解決方案厌丑,它也不是多么遙不可及的內(nèi)容定欧,很有可能大家在日常的生產(chǎn)生活中無數(shù)次的用到過。
只是沒有給起個響亮的名字罷了怒竿。
GOF總結(jié)的設(shè)計模式共有23種砍鸠,在這里我并不想去討論所有23種設(shè)計模式。
我的宗旨耕驰,技術(shù)是為了解決問題而存在的,正好饭弓,我現(xiàn)在存在一個場景弟断,我認為用享元模式和職責(zé)鏈來重構(gòu)會更加優(yōu)雅。
雖然我之前看過它們的介紹昏翰,但并沒有應(yīng)用于實踐中過棚菊,于是這篇筆記隨之而生叔汁。
這篇筆記的討論點將偏向于設(shè)計原則和技巧据块。
范式方面將著重介紹享元模式。
和之前的所有筆記一般,這篇筆記具備偏頗和主觀兩個特點浪谴,旨在為我在實踐中遇到的問題提供解決思路因苹。
設(shè)計模式漫談-原則與技巧
說到設(shè)計原則扶檐,常聽到的無非幾種:單一職責(zé)原則(SRP),最少知識原則(LKP)智蝠,開放封閉原則(OCP)杈湾。
談到技巧攘须,最常聽見的大概就是面向?qū)ο螅蛘呙嫦蚪涌诤费矗苍S還有函數(shù)式編程了至会。
它們通常如字面意思一般,概念上很簡單明了健霹,但是實踐起來可謂博大精深糖埋。
駕馭不了的話窃这,原本為了便于維護引用的原則可能會使代碼復(fù)雜度加深杭攻,反而變得不那么好維護,很多設(shè)計模式在實踐中也會有類似的問題馆铁。
這并不是說埠巨,它們不對现拒,或者不好,僅僅是你自身暫時達不到那么境界所以應(yīng)用不當勋桶。
實際上在我初學(xué)JavaScript之后緊接著就進行了有關(guān)Js設(shè)計模式的學(xué)習(xí)例驹,這也是為什么我能意識到眠饮,我最近遇到的場景可以使用這兩個模式重構(gòu)铜邮。
然而寨蹋,在相當長的時間里已旧,我并沒有機會在實踐中應(yīng)用它們运褪,無非能力不足秸讹,同事并不在意這些雅倒,時間不夠這么幾個因素。
這是很常見的劣欢,當你挖空心思優(yōu)化一個模塊凿将,而之后在這個模塊做擴展的同事并不賣賬价脾,簡單粗暴的怎么方便怎么來侨把。
不出一些時日,你之前的優(yōu)化還不如不做,因為它們看起來更難懂华匾,而功能又都差不多机隙。
從單一職責(zé)原則說起:
單一職責(zé)原則簡單來說就是:一個對象(方法)只做一件事情有鹿。
舉例來說,假如你想用爬蟲爬取某個網(wǎng)站的圖片,大概需要以下幾個過程持寄。
1、請求目標地址废麻,獲得響應(yīng)的到Html結(jié)構(gòu)
2模庐、解析Html結(jié)構(gòu)掂碱,獲得圖片地址
3、下載圖片地址
如果你想沧卢,你可以把它們寫成一個巨大的函數(shù)搏恤,只需要傳入一個Url就能自動下載到圖片湃交。
function getImg(url){
.....
}
//執(zhí)行完就能下載一張圖片搞莺。
然而這不是一個函數(shù)應(yīng)該干的事情,它不利于維護迈喉,如果應(yīng)用單一職責(zé)原則的話挨摸,它需要被拆分成至少以下幾個函數(shù)岁歉。
getHtml //請求URL地址得到Html結(jié)構(gòu)
handleHtml //解析Html獲得Img下載地址
downImg //下載圖片
//你可以寫一個對象锅移,加上start函數(shù),就像這樣置逻。
var main={
start:(url)=>{
var html= main.getHtml(url);
var imglist = main.handleHtml(html);
downImg(imgList);
},
getHtml:(url)=>{
....
},
handleHtml(html)=>{
...
},
downImg(imgList)=>{
...
}
}
當然也可以寫成單獨的函數(shù)不寫進一個Obj中券坞,它們明顯要比維護一個龐大復(fù)雜的函數(shù)要明智的多,當你想爬取另一個網(wǎng)站時深浮,只需替換中間的Html解析部分就可以了眠冈。
想擴展也可以很輕易的找到需要添加的位置蜗顽,或者需要修改的函數(shù)。
看起來似乎很簡單忿等,然而單一職責(zé)模式并不是那么容易被正確應(yīng)用贸街,因為并不是所有職責(zé)都應(yīng)該分離狸相,而且它雖然帶來了穩(wěn)定性脓鹃,但是也使得編寫代碼的復(fù)雜度加深,同時增加了這些對象之間聯(lián)系的難度,這有時也會使得這些對象并不那么易用娇跟。
最少知識原則
最少知識原則簡單來說則是:盡量減少對象之間的交互苞俘,如果兩個對象之間不必直接通信那么這兩個對象就不要直接相互聯(lián)系龄章。
通常使用中介對象來做聯(lián)系就是它的應(yīng)用了瓦堵,當某一部分發(fā)生更改只需要更改中介對象中的就可以了菇用。
舉例來說惋鸥,上面那個爬蟲列子start就是一個中介對象,我不關(guān)心你在爬取下載前經(jīng)過了多少步驟卦绣,我只是想下個圖片滤港,大概就是這個意思。
總結(jié)來說:最小知識原則減少了對象之間的依賴山叮,但有可能會制造一個龐大到難以維護的第三方對象屁倔,所以在實際使用中還需要分情況討論暮胧。
開放-封閉原則。
定義:軟件實體(類钞翔,模塊嗅战,函數(shù))等應(yīng)該是可以擴展的俺亮,但是不可修改脚曾。
這條概念好像有些難以理解,其實就是珊泳,當你需要新加一個需求的時候色查,在原系統(tǒng)上增加新的代碼撞芍,而不是去修改現(xiàn)用的模塊序无。
因為修改模塊的話衡创,有可能Bug會越改越多璃氢。
實例:
//假如你有一個非常龐大的If else 或者switch case分支一也。
//這明顯違背開放封閉原則隘竭,因為要擴展它不可避免的要去修改原本的代碼。
//來一段經(jīng)典的鴨子發(fā)聲作為實例尊剔。
function makeSound(animal){
switch(animal){
case 'duck':
console.log('嘎嘎嘎');
break;
case 'dog':
console.log('汪汪汪')
.....
}
}
//簡單的動物發(fā)聲须误,要是擴展的話仇轻,就得對這個switch case語句下手,當然這個列子看起來修改這個switch case似乎沒什么困難的祭椰,這里只是為了舉例方淤。
function makeSound(animal){
animal.sound();
}
function Duck(){
}
Duck.prototype.sound=()=>{
console.log('嘎嘎嘎');
}
makeSound(new Duck);
//擴展的話蹄殃,寫新的函數(shù)就可以了
//只是我覺得,要我寫的話..
function makeSound(animal){
makeSound.prototype[animal].sound();
}
makeSound.prototype.Duck={
sound:()=>{
console.log('嘎嘎嘎');
}
}
makeSound('Duck')
//擴展的話給它的protoype掛新的函數(shù)就行了讳苦,也算是不去修改原函數(shù)鸳谜。
上面是利用多態(tài)來重構(gòu)switch case 使它符合開放封閉原則以增加可擴展性式廷。
核心原則就一個,找出程序要發(fā)生變化的地方草描,然后把變化封裝起來穗慕。
常用的方式:使用鉤子逛绵,或者使用回調(diào)倔韭。
在這里不再展開,因為對此我也是一知半解胰苏,除了這個重構(gòu)選擇暫時沒想到新的應(yīng)用場景硕并,缺乏實踐動力秧荆。
實踐完職責(zé)鏈也許會對其理解更深。
以接口和面向接口編程結(jié)束
定義:接口是對象能響應(yīng)的請求的集合陕赃。
說API估計沒有程序員不知道它是啥么库,那什么是面向接口編程呢豌蟋?
簡單來說就是,只關(guān)心這些調(diào)用的類能做什么而不關(guān)心怎么做允睹。
在我來說缭受,則是當別人來引用你開發(fā)的大模塊中某一個小模塊時米者,它并不需要關(guān)注你的實現(xiàn),只需要知道調(diào)用會產(chǎn)生什么結(jié)果胰丁。
以上面的爬蟲列子來說喂分,就是無論你只是想獲得一堆Html蒲祈,還是想按一定的規(guī)則解析Html,還是只是想下載一個圖片扬卷,都可以引用上面那個大模塊中的某個函數(shù)來達成目的怪得,而不需要在意其它函數(shù)是怎么實現(xiàn)的。
具體展開似乎有一大串汇恤,介于我個人不是太感興趣拔恰,摘抄兩句颜懊,這個部分我們就先跳過吧。
面向接口編程匠璧,而不是面向?qū)崿F(xiàn)編程夷恍。
如果它走起路來像鴨子媳维,叫起來也是鴨子,那么它就是鴨子指黎。
前者在我看來和面向?qū)ο蟛o二致醋安,后者則可以參考下Js中龐大的類數(shù)組形數(shù)據(jù)調(diào)用數(shù)組的方法,這些接口并不關(guān)心它們拿到的是不是一個真正的數(shù)組亲怠。
享元模式
享元(flyweight)是一種性能優(yōu)化的模式赁炎,'fly'在這里是蒼蠅的意思,意為蠅量級讥裤。享元模式的核心是運用共享技術(shù)來有效支持大量細粒度的對象。
重點是劃分內(nèi)外狀態(tài)间螟,目的是減少共享對象的數(shù)量厢破。
我不會舉那個男女模特和內(nèi)衣工廠的列子
簡單復(fù)述一下就是摩泪,如果用代碼實現(xiàn)男女模特穿一千件內(nèi)衣拍照劫谅,直接寫的話捏检,往往會需要男女模特各一百個。
而稍微注意一點熊楼,則可以鲫骗,男女模特各一個換著穿一千件內(nèi)衣拍照悲雳,有興趣的可以自行百度。
經(jīng)驗指引:
- 內(nèi)部狀態(tài)存儲于對象內(nèi)部
- 內(nèi)部狀態(tài)可以被一些對象共享
- 內(nèi)部狀態(tài)獨立于具體場景坦胶,通常不會改變峭咒。
- 外部狀態(tài)取決于具體場景凑队,并根據(jù)場景而變化幔翰,外部狀態(tài)不能被共享遗增。
在這里我附上一個進程池的列子:
進程池的場景很常見做修,比如之前的Redis,NodeRedis庫每次讀取會重新連接蔗坯,如果多次連接宾濒,每次都新開一個連接很明顯不夠高效鼎兽。
正確的做法是開一個連接谚咬,將這個連接對象依次往下傳遞直到所有的讀取都結(jié)束為止择卦。
有并發(fā)的存在那么就開三個郎嫁。
其它泽铛,我以前做過一個在地圖上標小氣泡的頁面盔腔。
正常的思路是,獲取用戶定位地點為中心的圓宁赤,標注氣泡决左,移動佛猛,重新標注坠狡,或添加新的氣泡挚躯。
假如重新標注,清除之前的標注對象擦秽,再新建也很顯然不夠高效。
正確的做法是回收它們漩勤,再重新定位它們的位置感挥。
附上一個簡單實現(xiàn):
function divFactory(){
var divPool=[];
return{
create:this.create,
pool:divPool
}
}
divFactory.protoype.create=function(num){
var needDiv = num;
var result = [];
if(this.pool.length<needDiv){
var neddLength = needDiv - this.pool.length;
for(var i= 0;i<needLength;i++){
var div = document.createElement('div');
this.pool.push('div');
}
result = this.pool
}else{
for(var i=0;i<needDiv;i++){
result.push(this.pool[i])
}
}
return result;
}
//調(diào)用
var createFactory = new divFactory();
var res = createFactory.create(3);
for( i in res){
res[i].innerHTML = 'Create '+i;
document.body.appendChild(res[i]);
}
//頁面上出現(xiàn)3個寫著Create 的Div還有編號
var res2 = createFactory.create(5);
for(i in res2){
var tip = res[i].innerHTML || 'No'
res2[i].innerHTML=tip+'Create2'+i;
document.body.appendChild(res[i]);
}
//頁面上出現(xiàn)五個div其中前3個有Create+ 編號
上面這個列子以地圖上的小泡為列的話,可以重新修改已經(jīng)創(chuàng)建的小泡的名稱和位置越败,以圖性能優(yōu)化触幼。
附帶一提,當年我負責(zé)的那個項目這個地圖部分(不是我做的)最后選擇了一次性將所有可能出現(xiàn)的點都導(dǎo)到地圖上究飞,一次性標注完成置谦,不管你客戶滑到哪如果存在肯定能看到應(yīng)該有的點亿傅,全國差不多幾千個吧谅阿。
前兩天我還特的去看了眼,依舊是這個做法氯檐,它已經(jīng)平穩(wěn)運行兩年了糯崎,側(cè)面說明,前端優(yōu)化,有的時候并沒有面試時強調(diào)的那么重要黄锤,當然我還是提倡能優(yōu)化自然要優(yōu)化一下负甸。
結(jié)語
這篇筆記花的時間比之前任意一篇筆記的時間都長,原本我還以為可以記錄下職責(zé)鏈和代碼重構(gòu)奏篙,但事實證明敛熬,我下一篇筆記也許有內(nèi)容了。
編程范式這類的,其實和代碼編寫規(guī)范一般改备,沒有標準答案,而且往往需要分情況討論碉渡,我作為經(jīng)驗尚淺的開發(fā)人員是不敢妄議的。
但它既然經(jīng)過了這么多年的檢驗习霹,自是有歷史沉淀在里面的伪阶。
作為一個土木系畢業(yè)的工科生對這些工程化模塊化有天然的好感。
作為一個半路出家的前端癣缅,構(gòu)造函數(shù)和原型鏈依舊還不習(xí)慣使用。
設(shè)計模式博大精深,而這篇筆記怕還不能揭示其中的一鱗半爪,然作為我個人的學(xué)習(xí)記錄已經(jīng)足夠敛摘。
如若在將來能起到幫助它人理解之效捕虽,那便是倍感欣慰房揭,念及此雖并無關(guān)聯(lián)伶唯,而今天我便可放心的擱下鍵盤粹断。