Серия «Рефакторинг и шаблоны» (1) «Упрощенные функции»

внешний интерфейс Шаблоны проектирования JavaScript дизайн

Как долго функция уместна?

Functions should do one thing
Каждая функция должна делать только одну вещь. Если функция делает слишком много вещей, она крайне нестабильна. Такая связь приводит к слабой связности и хрупкости дизайна, и ее нелегко поддерживать и читать.
В рутинном человеческом мышлении всегда есть привычка складывать воедино набор связанных поведений, как разделить по отдельности не так просто.

Functions should only be one level of abstraction
Если в этой функции есть многошаговая операция, эти операции над одной и той же абстракцией, то мы все равно думаем, что эта функция только одна.

Посмотрите красивую демонстрацию:

Нам нужно увидеть наших пользователей из нашего списка зарегистрированных пользователей, отфильтровать активных пользователей и отправить им электронное письмо.

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();
}

Разборка слишком длинных функций, какие есть доступные режимы

Мы хотим упростить слишком длинные функции, так какие шаблоны мы можем использовать для оптимизации? В книге "Рефакторинг и шаблоны" упоминается, что перед лицом слишком длинных функций мы можем рассмотреть возможность использования следующих шаблонов:

  • Функция уточнения
  • Узор стратегии (заменить слишком много, если условные ветви)
  • Замыкания и функции высшего порядка
  • Шаблон метода шаблона

Функция уточнения

Общая идея извлечения функций состоит в том, чтобы разбить наши чрезмерно длинные функции на небольшие фрагменты функций, чтобы гарантировать, что обработка фрагментов функций в измененной функции находится на одном уровне. Я только что нашел пример в обычном компоненте изображения предварительного просмотра.

Структура 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.

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 и практика разработки», стр. 84.Примеры формы правил проверки

Воспользуйтесь преимуществами функций и замыканий высшего порядка

Закрытие

Избегайте объявления многих глобальных переменных, через замыкания мы храним переменные

// 利用高阶函数避免写全局变量
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 относится к динамическому встраиванию функции в другую функцию, например

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();

Использование шаблонного метода и описанного выше АОП в бизнесе может эффективно отделить наш код, например, следующий 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