Как долго функция уместна?
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