函式多長合適?
Functions should do one thing
每一個函式應該都只做一件事情,如果一個函式做了過多的事情,那麼它極為不穩定,這種耦合性得到的是低內聚和脆弱的設計,且不方便維護與閱讀。
在人的常規思維中,總是習慣把一組相關的行為放到一起,如何正確的進行分離並不容易。Functions should only be one level of abstraction
如果這個函式內有多步操作,但這些操作都在同一個抽象層上,那麼我們還是認為這個函式只做了一件事
看個不錯的demo:
我們需要從我們的註冊使用者列表中檢視我們的使用者,並且篩選出活躍使用者,並向他們傳送一封郵件。
Bad:
// 一般我們的書寫風格 按照邏輯順序寫下去, 幾件事情雜糅在了一起
function emailClients(clients) {
clients.forEach(function (client, index) {
var _clientRecord = database.lookup(client);
if(_clientRecord.isActive()){
email(client);
}
})
}複製程式碼
Good
// 讓一個函式只幹一件事情 單一職責
function emailClients(clients) {
clients.filter(isClientActive)
.forEach(email)
}
function isClientActive(client) {
var _clientRecord = database.lookup(client);
return _clientRecord.isActive();
}複製程式碼
拆解過長函式,有哪些可用模式
我們要簡化過長的函式,那麼我們可以使用哪些模式來優化?《重構與模式》一書中提到面對過長的函式,我們可以考慮使用下面幾種模式:
- 提煉函式
- 策略模式(去替換過多的if條件分支)
- 閉包與高階函式
- 模板方法模式
提煉函式
提煉函式大致的思想就是將我們過長的函式拆分為小的函式片段,確保改函式內的函式片段的處理在同一層面上。隨便找了一個regular預覽圖片元件裡面的例子。
HTML結構如下:
{#if showPreview}
<div class="m-image-gallery-mask"></div>
<ul
class="m-image-gallery"
style="-webkit-transform: translate3d({ wrapperOffsetX }px,0,0);"
ref="wrapper" on-click={this.onClose($event)}>
{#if prev}
<li
class="m-image-gallery-item"
style="-webkit-transform: translate3d(-{ windowWidth }px,0,0);width: { windowWidth }px;"
ref="prev">
<div class="m-image-gallery-img-wrapper">
<img class="m-image-gallery-img" src="{ prev || _1x1 }" alt="">
</div>
</li>
{/if}
<li class="m-image-gallery-item" style="width: { windowWidth }px;" ref="current">
<div class="m-image-gallery-img-wrapper">
<img
class="m-image-gallery-img"
style="-webkit-transform: scale({ scale }) translate3d({ offsetX }px,{ offsetY }px,0);"
src="{ current || _1x1 }"
on-load="{ this.onCurrentLoaded() }"
alt="預覽圖"
ref="v"/>
</div>
</li>
{#if next}
<li class="m-image-gallery-item" style="-webkit-transform: translate3d({ windowWidth }px,0,0);transform: translate3d({ windowWidth }px,0,0);width: { windowWidth }px;" ref="next">
<div class="m-image-gallery-img-wrapper">
<img class="m-image-gallery-img" src="{ next || _1x1 }" alt="">
</div>
</li>
{/if}
</ul>
{/if}複製程式碼
onTouchMove: function (e) {
// 觸控touchmove
var _touches = e.touches,
_ret = isEdgeWillAway(_v),
_data = this.data;
e.preventDefault();
(!this.touchLength) && (this.touchLength = e.touches.length);
if (this.touchLength === 1) {
this.deltaX = _touches[0].pageX - this.initPageX;
this.deltaY = _touches[0].pageY - this.initPageY;
if (_ret.left) {
// 圖片將要往右邊移動
_data.wrapperOffsetX = this.startOrgX + this.deltaX;
_data.prevShow = true;
} else if (_ret.right) {
// 圖片將要往左邊移動
_data.wrapperOffsetX = this.startOrgX + this.deltaX;
_data.nextShow = true;
}
this.$update();
}else if (this.touchLength === 2) {
//如果是兩個手指 進行縮放控制
....
}
},複製程式碼
可以看到在touchMove的函式很長,我們需要對這個函式進行提煉, 大致應該是下面這樣
onTouchMove: function(e){
// 觸控touchmove
var _touches = e.touches,
_ret = isEdgeWillAway(_v),
_data = this.data;
e.preventDefault();
(!this.touchLength) && (this.touchLength = e.touches.length);
if ( this.touchLength === 1 ) {
// this.$emit('setTranslate');
// 移動圖片
this.setMove(...);
}else if ( this.touchLength === 2) {
// this.$emit('setScale');
// 縮放圖片
this.setScale(...);
}
}複製程式碼
包括一些事件的繫結,我們通過下面的書寫方式相比於直接寫cb function也能更好地解耦。
initEvent: function () {
if (!this.data.showPreview) return;
_wrapper.addEventListener('touchstart', this.onTouchStart.bind(this));
_wrapper.addEventListener('touchmove', this.onTouchMove.bind(this));
_wrapper.addEventListener('touchend', this.onTouchEnd.bind(this));
this.$on('animateLeft', this.onAnimateLeft.bind(this));
this.$on('animateRight', this.onAnimateRight.bind(this));
this.$on('animateReset', this.onAnimateReset.bind(this));
},
onTouchStart: function(){
.....
},複製程式碼
策略模式
當我們程式碼中有較多的if條件分支時,我們一般會選擇策略模式進行重構。
策略模式的核心就是封裝變化的部分,把策略的使用與策略的實現隔離開來,一個策略類,一個上下文類,依據不同的上下文返回不同的策略。
例如:
// 比如小球的動畫
// 策略類
var tween = {
//@params t: 已執行時間 b: 原始位置 c: 目標位置 d: 持續總時間
//@return 返回元素此時應該處於的位置
linear: function (t, b, c, d) {
return c * t / d + b;
},
easeIn: function (t, b, c, d) {
return c * (t/=d) * t + b
},
....
}
var Animation = function () {
}
Animation.prototype.start = function (target, config) {
var _timeId;
this.startTime = +new Date;// 開始時間
this.duration = config.duration;// 動畫持續時間
this.orgPos = target.getBoundingClientRect()[config.property];// 元素原始的位置
this.easing = tween[config.type];// 使用的動畫演算法
this.endPos = config.endPos;// 元素目標位置
_timeId = setInterval(function(){// 啟動定時器,開始執行動畫
if(!this.step()){// 如果動畫已經結束,清除定時器
clearInterval(_timeId);
}
}.bind(this), 16);
}
Animation.prototype.step = function () {
var _now = +new Date,// 當前時間
_dur = _now - this.startTime,// 已執行時間
_endPos;
_endPos = this.easing(_dur, this.orgPos, this.endPos, this.duration);// 此時應該在的位置
this.update(_endPos);// 更新小球的位置
}複製程式碼
類似的,其他經典的例子還有驗證規則的策略模式的寫法。
可以看下 《Javascript設計模式與開發實踐》P84 表單規則校驗的例子
善用高階函式和閉包
閉包
避免宣告許多全域性變數,通過閉包我們來儲存變數
// 利用高階函式避免寫全域性變數
pro.__isWeiXinPay = (function(){
var UA = navigator.userAgent;
var index = UA.indexOf("MicroMessenger");
var _isWeiXinPay = (index!=-1 && +UA.substr(index+15,3)>=5);
// window._isWeiXin = index!=-1;
return function(){
return _isWeiXinPay;
}
})();複製程式碼
高階函式
高階函式是指至少滿足下列條件之一的函式:
- 函式可以作為引數被傳遞
- 函式可以作為返回值輸出
高階函式在我們編碼時無形中被使用,善用高階函式可以使我們程式碼寫的更加漂亮。
通過高階函式實現AOP
在Js中實現AOP,都是指把一個函式動態織入到另一個函式中,比如
Function.prototype.before = function (beforefn) {
var _self = this;
return function () {
beforefn.apply(this, arguments);// 執行before函式
return _self.apply(this, arguments);// 執行本函式
}
}
Function.prototype.after = function (beforefn) {
var _self = this;
return function () {
var ret = _self.apply(this, arguments);// 執行本函式
afterfn.apply(this, arguments);// 執行before函式
return ret;
}
}
var func = function () {
console.log('hahaha');
};
func = func.before(function(){
console.log(1);
}).after(function(){
console.log(2);
})複製程式碼
有了aop以後,可以幫助我們把原來耦合在一起的長函式進行拆解,再利用模板模式我們可以達到意想不到的效果,見下節。
模板方法模式
如果我們有一些平行的子類, 各個子類之間有一些相同的行為,也有一些不同的行為。相同的行為可以被搬移到另外一個單一的地方。在模板方法模式中,子類實現中相同的部分可以上移到父類,而將不同的部分待由子類去實現。模板方法就是這樣的模式。
模板方法模式由兩部分組成,抽象類和實現類,我們把抽出來的共同部分放到抽象類中,變化的方法抽成抽象方法,方法的具體實現由子類去實現,先看《設計模式實踐》書中的一個例子:
var Beverage = function(param){
var boilWater = function () {
console.log('把水煮開');// 共同的方法
};
var brew = param.brew || function(){
throw new Error('必選傳遞brew方法');// 需要子類具體實現
};
var pourInCup = param.pourInCup || function(){
throw new Error('必選傳遞pourInCup方法');
};
var addCondiments = param.addCondiments || function(){
throw new Error( '必選傳遞addCondiments方法' );
};
var F = function(){}
F.prototype.init = function(){
boilWater();
brew();
pourInCup();
addCondiments();
}
return F;
}
var Coffee = Beverage({
brew: function(){
console.log('用沸水泡咖啡');
},
pourInCup: function(){
console.log('把咖啡倒進杯子');
},
addCondiments: function(){
console.log('加糖和牛奶');
}
});
var Tea = Beverage({
brew: function(){
console.log('用沸水泡茶葉');
},
pourInCup: function(){
console.log('把茶倒進杯子');
},
addCondiments: function(){
console.log('加檸檬');
}
});
var coffee = new Coffee();
coffee.init();
var tea = new Tea();
tea.init();複製程式碼
在業務中使用模板方法和上面的AOP我們可以將我們的程式碼有效解耦,例如下面的rgl.module.js 是所有自定義模組的基類。
var BaseList = BaseComponent.extend({
config: function () {
this.data.loading = true;
},
initRequestEvents: function () {
var data = this.data,
dataset = data.dataset||{};
data.requestUrl = this.getRequestUrl(dataset);
this.onRequestCustomModuleData(data.requestUrl);
},
onRequestCustomModuleData: function () {
if(!requestUrl) return;
var self = this,
data = this.data;
this.$request(requestUrl,{
method: 'GET',
type: 'json',
norest: true,
onload: this.cbRequestCustomModuleData._$bind(this)._$aop(function(){
if(data.loadingElem && data.loadingElem[0]) e._$remove(data.loadingElem[0]);
},function(){
self.finishRequestData();
}),// 這裡就是模板模式方法與aop的結合使用
onerror: function(){
data.loading = false;
}
});
},
cbRequestCustomModuleData: f,// 提供給子類具體實現的介面 子類繼承BaseComponent自己具體實現
finishRequestData: f // 提供給子類具體實現的介面 子類繼承BaseComponent自己具體實現
});複製程式碼
var BottomModule = BaseModule.extend({
template: tpl,
config: function(data){
_.extend(data, {
clickIndexArray:[],
isStatic: false
});
},
init: function(){
this.initRequestEvents();
},
cbRequestCustomModuleData: function(data){
......// 具體實現
}
};複製程式碼
下一節將會看看重複程式碼的問題及可參考模式。
【參考書籍】
- 《Javascript 設計模式與開發實踐》
- Clean Code in Javascript