重構與模式系列(一)簡化函數

函數多長合適?

Functions should do one thing
每個函數應該都只作一件事情,若是一個函數作了過多的事情,那麼它極爲不穩定,這種耦合性獲得的是低內聚和脆弱的設計,且不方便維護與閱讀。
在人的常規思惟中,老是習慣把一組相關的行爲放到一塊兒,如何正確的進行分離並不容易。javascript

Functions should only be one level of abstraction
若是這個函數內有多步操做,但這些操做都在同一個抽象層上,那麼咱們仍是認爲這個函數只作了一件事html

看個不錯的demo:java

咱們須要從咱們的註冊用戶列表中查看咱們的用戶,而且篩選出活躍用戶,並向他們發送一封郵件。git

Bad:github

// 通常咱們的書寫風格 按照邏輯順序寫下去, 幾件事情雜糅在了一塊兒
function emailClients(clients) {
  clients.forEach(function (client, index) {
    var _clientRecord = database.lookup(client);
    if(_clientRecord.isActive()){
      email(client);
    }
  })
}複製代碼

Goodweb

// 讓一個函數只幹一件事情 單一職責
function emailClients(clients) {
      clients.filter(isClientActive)
           .forEach(email)    
}

function isClientActive(client) {
  var _clientRecord = database.lookup(client);
  return _clientRecord.isActive();
}複製代碼

拆解過長函數,有哪些可用模式

咱們要簡化過長的函數,那麼咱們可使用哪些模式來優化?《重構與模式》一書中提到面對過長的函數,咱們能夠考慮使用下面幾種模式:算法

  • 提煉函數
  • 策略模式(去替換過多的if條件分支)
  • 閉包與高階函數
  • 模板方法模式

提煉函數

提煉函數大體的思想就是將咱們過長的函數拆分爲小的函數片斷,確保改函數內的函數片斷的處理在同一層面上。隨便找了一個regular預覽圖片組件裏面的例子。json

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){
              ......// 具體實現
            }
};複製代碼

下一節將會看看重複代碼的問題及可參考模式。

【參考書籍】

相關文章
相關標籤/搜索