Шаблоны проектирования, используемые в исходном коде фреймворка для улучшения расширяемости.

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

Зачем улучшать масштабируемость кода

Код, который мы пишем, отвечает определенным требованиям, но эти требования не являются статичными.Когда требования меняются, если масштабируемость нашего кода очень хорошая, нам может потребоваться просто добавить или удалить модули.Если производительность не очень хорошая, все код может потребоваться переписать, что является катастрофой, поэтому крайне важно улучшить масштабируемость кода. Как добиться хорошей масштабируемости? Хорошая масштабируемость должна иметь следующие характеристики:

  1. Когда требования меняются, код не нужно переписывать.
  2. Модификации локального кода не вызывают масштабных изменений. Иногда мы идем на рефакторинг небольшого куска кода, но обнаруживаем, что он перемешан с другими кодами, и есть разные сцепки.Одно дело делается в нескольких местах.Если хочешь изменить этот маленький кусок, надо изменить многое. другой код. Это означает, что связь этих кодов слишком высока, а масштабируемость невелика.
  3. Новые функции и новые модули могут быть легко введены.

Как улучшить масштабируемость кода?

Конечно, я учился на отличном коде, эта статья пойдет глубже.Axios,Node.js,VueДождитесь отличных фреймворков, обобщите несколько шаблонов проектирования из их исходного кода, а затем используйте эти шаблоны проектирования, чтобы попытаться решить проблемы, возникшие в следующей работе. В этой статье в основном речь пойдет о职责链模式,观察者模式,适配器模式,装饰器模式. Давайте посмотрим ниже:

Схема цепочки ответственности

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

Пример: перехватчик Axios

Друзья, которые использовали Axios, должны знать, что у перехватчика Axios请求拦截器а также响应拦截器, порядок выполнения请求拦截器 -> 发起请求 -> 响应拦截器, что на самом деле представляет собой цепочку из трех обязанностей. Давайте посмотрим, как реализована эта цепочка:

// 先从用法入手,一般我们添加拦截器是这样写的 
// instance.interceptors.request.use(fulfilled, rejected)
// 根据这个用法我们先写一个Axios类。
function Axios() {
  // 实例上有个interceptors对象,里面有request和response两个属性
  // 这两个属性都是InterceptorManager的实例
  this.interceptors = {
    request: new InterceptorManager(),
    response: new InterceptorManager()
  };
}

// 然后是实现InterceptorManager类
function InterceptorManager() {
  // 实例上有一个数组,存储拦截器方法
  this.handlers = [];
}

// InterceptorManager有一个实例方法use
InterceptorManager.prototype.use = function(fulfilled, rejected) {
  // 这个方法很简单,把传入的回调放到handlers里面就行
  this.handlers.push({
    fulfilled,
    rejected
  })
}

Приведенный выше код фактически завершает создание перехватчика иuseЛогика не сложная, так когда выполняются эти методы перехватчика? Конечно, мы звонимinstance.requestпри звонкеinstance.requestкогда реальная реализация请求拦截器 -> 发起请求 -> 响应拦截器цепочка, поэтому нам также необходимо реализовать следующуюAxios.prototype.request:

Axios.prototype.request = function(config) {
  // chain里面存的就是我们要执行的方法链条
  // dispatchRequest是发起网络请求的方法,本文主要讲设计模式,这个方法就不实现了
  // chain里面先把发起网络请求的方法放进去,他的位置应该在chain的中间
  const chain = [dispatchRequest, undefined];
  
  // chain前面是请求拦截器的方法,从request.handlers里面取出来放进去
  this.interceptors.request.handlers.forEach(function unshiftRequestInterceptors(interceptor) {
    chain.unshift(interceptor.fulfilled, interceptor.rejected);
  });
  
  // chain后面是响应拦截器的方法,从response.handlers里面取出来放进去
  this.interceptors.response.handlers.forEach(function pushResponseInterceptors(interceptor) {
    chain.push(interceptor.fulfilled, interceptor.rejected);
  });
  
  // 经过上述代码的组织,chain这时候是这样的:
  // [request.fulfilled, request.rejected, dispatchRequest, undefined, response.fulfilled,  
  // response.rejected]
  // 这其实已经按照请求拦截器 -> 发起请求 -> 响应拦截器的顺序排好了,拿来执行就行
  
  let promise = Promise.resolve(config);   // 先来个空的promise,好开启then
  while (chain.length) {
    // 用promise.then进行链式调用
    promise = promise.then(chain.shift(), chain.shift());
  }

  return promise;
}

Приведенный выше код взят изИсходный код AxiosВидно, что он умело использует модель цепочки ответственности, чтобы организовать задачи, которые необходимо выполнить, в цепочку. Задачи в этой цепочке не влияют друг на друга. Перехватчики необязательны, и их может быть несколько, совместимых. очень сильный.

Пример: проверка формы организации цепочки ответственности

Я видел выдающуюся основа к обязанностям, давайте посмотрим, как эта модель используется в нашей обычной работе. Теперь предположим, что существует такое требование быть проверкой формы, эта проверка требует проверки для проверки формата и т. Д., А затем API отправит проверку легитимности. Сначала мы анализируем этот спрос, предельный чек синхронизирован, верификация Backend является асинхронной, весь процесс представляет собой синхронные асинхронные чередулки, чтобы быть совместимым, возвращаемое значение каждого метода проверки должно быть упаковано в обещание талантливых

// 前端验证先写个方法
function frontEndValidator(inputValue) {
  return Promise.resolve(inputValue);      // 注意返回值是个promise
}

// 后端验证也写个方法
function backEndValidator(inputValue) {
  return Promise.resolve(inputValue);      
}

// 写一个验证器
function validator(inputValue) {
  // 仿照Axios,将各个步骤放入一个数组
  const validators = [frontEndValidator, backEndValidator];
  
  // 前面Axios是循环调用promise.then来执行的职责链,我们这里换个方式,用async来执行下
  async function runValidate() {
    let result = inputValue;
    while(validators.length) {
      result = await validators.shift()(result);
    }
    
    return result;
  }
  
  // 执行runValidate,注意返回值也是一个promise
  runValidate().then((res) => {console.log(res)});
}

// 上述代码已经可以执行了,只是我们没有具体的校验逻辑,输入值会原封不动的返回
validator(123);     // 输出: 123

В приведенном выше коде мы используем режим цепочки обязанностей для организации логики многократной проверки, и эти значения не имеют зависимостей, и если вам нужно уменьшить проверку, вам нужно толькоvalidatorsВы можете удалить его из массива, а если хотите добавить, просто добавьте в массив. Связь между этими валидаторами значительно уменьшена, и они инкапсулируют промисы, которые также можно использовать в других модулях.Другие модули могут организовывать свои собственные цепочки ответственности по мере необходимости.

Шаблон наблюдателя

Режим наблюдателя также называют режимом публикации-подписки, который известен в мире JS. Все в той или иной степени его использовали. Самый распространенный из них — привязка событий. Некоторые интервью также требуют, чтобы интервьюируемый написал событие вручную. на самом деле является шаблоном наблюдателя. Преимущество паттерна наблюдателя в том, что производители и потребители событий не знают друг друга, и им нужно только генерировать и потреблять соответствующие события.Это особенно подходит для ситуаций, когда производители и потребители событий неудобно вызывать напрямую, например как при асинхронности. Давайте напишем шаблон наблюдателя, чтобы увидеть:

class PubSub {
  constructor() {
    // 一个对象存放所有的消息订阅
    // 每个消息对应一个数组,数组结构如下
    // {
    //   "event1": [cb1, cb2]
    // }
    this.events = {}
  }

  subscribe(event, callback) {
    if(this.events[event]) {
      // 如果有人订阅过了,这个键已经存在,就往里面加就好了
      this.events[event].push(callback);
    } else {
      // 没人订阅过,就建一个数组,回调放进去
      this.events[event] = [callback]
    }
  }

  publish(event, ...args) {
    // 取出所有订阅者的回调执行
    const subscribedEvents = this.events[event];

    if(subscribedEvents && subscribedEvents.length) {
      subscribedEvents.forEach(callback => {
        callback.call(this, ...args);
      });
    }
  }

  unsubscribe(event, callback) {
    // 删除某个订阅,保留其他订阅
    const subscribedEvents = this.events[event];

    if(subscribedEvents && subscribedEvents.length) {
      this.events[event] = this.events[event].filter(cb => cb !== callback)
    }
  }
}

// 使用的时候
const pubSub = new PubSub();
pubSub.subscribe('event1', () => {});    // 注册事件
pubSub.publish('event1');                // 发布事件

Пример: EventEmitter для Node.js

Типичным применением шаблона наблюдателя является EventEmitter Node.js, у меня есть другая статьяПрочитайте и поймите исходный код EventEmitter Node.js из модели публикации-подписки.С точки зрения асинхронных приложений подробно объясняется принцип режима наблюдателя и исходный код Node.js EventEmitter.Я не буду повторять здесь написанное.Приведенный выше рукописный код также из этой статьи.

Пример: игра в лотерею

Точно так же, прочитав исходный код отличного фреймворка, мы должны сами попробовать его использовать.Пример здесь — круглая лотерея. Предположительно многие мои друзья разыгрывали призы онлайн.В нем есть вертушка с разными призами.Нажмите на розыгрыш, после чего указатель начнет вращаться и, наконец, остановится на призе. Наш пример — реализация такого демо, но есть также требование, чтобы скорость была немного выше для каждого оборота. Проанализируем это требование:

  1. Чтобы разыграть лотерею на проигрывателе, мы должны сначала нарисовать проигрыватель.
  2. В лотерее обязательно будет результат, есть ли приз или нет, какой конкретный приз, как правило, этот результат возвращается API, многие реализации должны щелкнуть по лотерее, чтобы инициировать запрос API для получения результат, а анимация круга — это просто эффект.
  3. Давайте напишем небольшой код, чтобы заставить проигрыватель двигаться, нам нужен эффект движения
  4. Нам нужно ускорять каждый ход, поэтому нам также нужно контролировать скорость движения.

Проведя приведенный выше анализ, мы обнаружили проблему.Поворотному столу требуется некоторое время для перемещения.Когда он заканчивает движение, он должен сообщить модулю, который управляет поворотным столом, чтобы ускорить движение следующего круга.Поэтому модуль движения и модуль управления нуждается в асинхронной связи.Эта асинхронная связь требует решения нашего шаблона наблюдателя. Конечный эффект выглядит следующим образом: поскольку это всего лишь DEMO, я заменил проигрыватель несколькими блоками DIV:

pubsub

Вот код:

// 先把之前的发布订阅模式拿过来
class PubSub {
  constructor() {
    this.events = {}
  }

  subscribe(event, callback) {
    if(this.events[event]) {
      this.events[event].push(callback);
    } else {
      this.events[event] = [callback]
    }
  }

  publish(event, ...args) {
    const subscribedEvents = this.events[event];

    if(subscribedEvents && subscribedEvents.length) {
      subscribedEvents.forEach(callback => {
        callback.call(this, ...args);
      });
    }
  }

  unsubscribe(event, callback) {
    const subscribedEvents = this.events[event];

    if(subscribedEvents && subscribedEvents.length) {
      this.events[event] = this.events[event].filter(cb => cb !== callback)
    }
  }
}

// 实例化一个事件中心
const pubSub = new PubSub();

// 总共有 初始化页面 -> 获取最终结果 -> 运动效果 -> 运动控制 四个模块
// 初始化页面
const domArr = [];
function initHTML(target) {
  // 总共10个可选奖品,也就是10个DIV
  for(let i = 0; i < 10; i++) {
    let div = document.createElement('div');
    div.innerHTML = i;
    div.setAttribute('class', 'item');
    target.appendChild(div);
    domArr.push(div);
  }
}

// 获取最终结果,也就是总共需要转几次,我们采用一个随机数加40(4圈)
function getFinal() {
  let _num = Math.random() * 10 + 40;

  return Math.floor(_num, 0);
}

// 运动模块,具体运动方法
function move(moveConfig) {
  // moveConfig = {
  //   times: 10,     // 本圈移动次数
  //   speed: 50      // 本圈速度
  // }
  let current = 0; // 当前位置
  let lastIndex = 9;   // 上个位置

  const timer = setInterval(() => {
    // 每次移动给当前元素加上边框,移除上一个的边框
    if(current !== 0) {
      lastIndex = current - 1;
    }

    domArr[lastIndex].setAttribute('class', 'item');
    domArr[current].setAttribute('class', 'item item-on');

    current++;

    if(current === moveConfig.times) {
      clearInterval(timer);

      // 转完了一圈广播事件
      if(moveConfig.times === 10) {
        pubSub.publish('finish');
      }
    }
  }, moveConfig.speed);
}

// 运动控制模块,控制每圈的参数
function moveController() {
  let allTimes = getFinal();
  let circles = Math.floor(allTimes / 10, 0);
  let stopNum = allTimes % circles;
  let speed = 250;  
  let ranCircle = 0;

  move({
    times: 10,
    speed
  });    // 手动开启第一次旋转

  // 监听事件,每次旋转完成自动开启下一次旋转
  pubSub.subscribe('finish', () => {
    let time = 0;
    speed -= 50;
    ranCircle++;

    if(ranCircle <= circles) {
      time = 10;
    } else {
      time = stopNum;
    }

    move({
      times: time,
      speed,
    })
  });
}

// 绘制页面,开始转动
initHTML(document.getElementById('root'));
moveController();

Сложность приведенного выше кода в том, что движение модуля движения асинхронно, и необходимо уведомлять модуль управления движением о выполнении следующего поворота после каждого круга движения.Режим наблюдателя очень хорошо решает эту проблему.Я загрузил полный код этого примера на свой GitHub, вы можете взять его и запустить.

Шаблон декоратора

Паттерн декоратор нацелен на ситуацию, что у меня есть какой-то старый код, но эти старые коды недостаточно функциональны, и мне нужно добавить функции, но я не могу изменить старый код, например, Vue 2.x нужно следить за изменениями массива и добавлять ему отзывчивости.Но он не может напрямую модифицироватьArray.prototype. В этом случае особенно удобно использовать шаблон декоратора, чтобы заново украсить старый метод и превратить его в новый метод для использования.

Базовая структура

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

var a = {
  b: function() {}
}

function myB() {
  // 先调用以前的方法
  a.b();
  
  // 再加上自己的新操作
  console.log('新操作');
}

Пример: прослушивание массивов Vue

Друзья, знакомые с принципом отзывчивости Vue, знают, что (Незнакомые друзья могут увидеть здесь), отзывчивость объектов Vue 2.x осуществляется черезObject.definePropertyРеализовано, но этот метод не может отслеживать изменения массива, так как же мониторит массив? Операции с массивами обычноpush,shiftЭти методы, эти методы являются нативными методами массива, мы, конечно, не можем их изменить, тогда будет использоваться паттерн декоратора, мы можем расширить его функции на основе сохранения его предыдущих функций:

var arrayProto = Array.prototype;    // 先拿到原生数组的原型
var arrObj = Object.create(arrayProto);     // 用原生数组的原型创建一个新对象,免得污染原生数组
var methods = ['push', 'shift'];    // 需要扩展的方法,这里只写了两个,但是不止这两个

// 循环methods数组,扩展他们
methods.forEach(function(method) {
  // 用扩展的方法替换arrObj上的方法
  arrObj[method] = function() {
    var result = arrayProto[method].apply(this, arguments);    // 先执行老方法
    dep.notify();     // 这个是Vue的方法,用来做响应式
    return result;
  }
});

// 对于用户定义的数组,手动将它的原型指向扩展了的arrObj
var a = [1, 2, 3];
a.__proto__ = arrObj;

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

Пример: расширение существующей привязки события

Старые правила, изучив чужой код, попробуем сами. Требование для этого примера состоит в том, что нам нужно добавить некоторые операции к существующим событиям кликов DOM.

// 我们以前的点击事件只需要打印1
dom.onclick = function() {
  console.log(1);
}

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

var oldFunc = dom.onclick;  // 先将老方法拿出来
dom.onclick = function() {   // 重新绑定事件
  oldFunc.apply(this, arguments);  // 先执行老的方法
  
  // 然后添加新的方法
  console.log(2);
}

Приведенный выше код расширенdom, но если есть много элементов DOM, которые нужно изменить, нам нужно перебиндить события одно за другим, и будет много похожего кода.Одна из целей изучения шаблонов проектирования — избежать дублирования кода, чтобы мы могли use common Операция привязки извлекается как декоратор:

var decorator = function(dom, fn) {
  var oldFunc = dom.onclick;
  
  if(typeof oldFunc === 'function'){
    dom.onclick = function() {
      oldFunc.apply(this, arguments);
      fn();
    }
  }
}

// 调用装饰器,传入参数就可以扩展了
decorator(document.getElementById('test'), function() {
  console.log(2);
})

Этот метод особенно подходит для сторонних компонентов пользовательского интерфейса, которые мы вводим.Некоторые компоненты пользовательского интерфейса инкапсулируют многие функции сами по себе, но не раскрывают интерфейс.Если мы хотим добавить функции и не можем напрямую изменить его исходный код, лучший способ - используйте его так. Шаблон декоратора можно расширить, а с помощью фабрики декорирования мы также можем быстро изменять его партиями.

режим адаптера

Адаптер, должно быть, использовался всеми. Старая видеокарта в моем доме имеет только интерфейс HDMI, а монитор - интерфейс DP. Эти два не могут быть подключены. Что мне делать? Ответ - купить адаптер и преобразовать интерфейс DP в HDMI. Принцип режима адаптера здесь аналогичен, когда мы сталкиваемся с ситуацией, что интерфейс не общий, параметры интерфейса не совпадают и т. д., мы можем обернуть вне его другой метод, который получает наше текущее имя и параметры, и вызывает старый метод для передачи предыдущих параметров form.

Базовая структура

Базовая структура шаблона адаптера выглядит следующим образом, предполагая, что функция журнала, которую мы хотим использовать, называетсяmylog, а конкретный метод мы хотим назвать уже готовымwindow.console.logЕсли он реализуется, то мы можем завернуть его в слой.

var mylog = (function(){
  return window.console.log;
})()

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

Пример: фреймворк изменен

Предположим, что проблема, с которой мы сталкиваемся сейчас, — это фреймворк А, который компания использовала раньше, но теперь решает заменить его на jQuery.Большинство интерфейсов двух фреймворков совместимы, но некоторые интерфейсы несовместимы, нам нужно решить эту проблему.

// 一个修改css的接口
$.css();      // jQuery叫css
A.style();    // A框架叫style

// 一个绑定事件的接口
$.on();       // jQuery叫on
A.bind();     // A框架叫bind

Конечно, мы можем изменить место использования с помощью глобального поиска, но может быть более элегантно использовать адаптер для модификации:

// 直接把以前用的A替换成$
window.A = $;

// 适配A.style
A.style = function() {
  return $.css.apply(this, arguments);    // 保持this不变
}

// 适配A.bind
A.bind = function() {
  return $.on.apply(this, arguments);
}

Адаптер такой простой, интерфейс другой, просто поменяйте слой пакета на такой же.

Пример: адаптация параметров

Режим адаптера может не только адаптироваться к несогласованности интерфейса, как указано выше, но и адаптироваться к разнообразию параметров. Если одному из наших методов нужно получить очень сложный объектный параметр, такой как конфигурация webpack, вариантов может быть много, но пользователь может использовать только часть, или пользователь может передать неподдерживаемую конфигурацию, тогда нам нужно переход пользователя Процесс адаптации конфигурации к стандартной конфигурации на самом деле очень прост:

// func方法接收一个很复杂的config
function func(config) {
  var defaultConfig = {
    name: 'hong',
    color: 'red',
    // ......
  };
  
  // 为了将用户的配置适配到标准配置,我们直接循环defaultConfig
  // 如果用户传入了配置,就用用户的,如果没传就用默认的
  for(var item in defaultConfig) {
    defaultConfig[item] = config[item] || defaultConfig[item];
  }
}

Суммировать

  1. Ядром высокой масштабируемости на самом деле является высокая связность и низкая связанность.Каждый модуль фокусируется на своих собственных функциях и сводит к минимуму прямую зависимость от внешнего мира.
  2. Режим цепочки ответственности и режим наблюдателя в основном используются для уменьшения связи между модулями. Когда связь низкая, их легко организовать и расширить их функции. Режим адаптера и режим декоратора в основном используются, не влияя на исходный расширенный на основании кода.
  3. Если нам нужен ряд операций для объекта, эти операции можно организовать в цепочку, тогда мы можем рассмотреть возможность использования режима цепочки обязанностей. Конкретным задачам в цепочке не нужно знать о существовании других задач, они фокусируются только на своей работе, а за передачу сообщения отвечает цепочка. Используя режим цепочки обязанностей, задачу в цепочке можно легко увеличивать, удалять или реорганизовывать в новую цепочку, как в конвейере.
  4. Если у нас есть два объекта в неопределенный момент времени, требующий асинхронной связи, мы можем рассмотреть возможность использования режима наблюдателя, пользователю не нужно беспокоиться о других конкретных объектах, если он зарегистрировал сообщение в центре сообщений, когда это сообщение появляется, Центр сообщений будет нести ответственность за его информирование.
  5. Если у нас есть какой-то старый код, но эти старые коды не могут удовлетворить наши потребности, и мы не можем изменить его по своему желанию, мы можем рассмотреть возможность использования шаблона декоратора для улучшения его функции.
  6. Для преобразования старого кода или внедрения новых модулей мы можем столкнуться с ситуацией, когда интерфейс не универсален, в это время мы можем рассмотреть возможность написания адаптера для их адаптации. Режим адаптера также применяется в случае адаптации параметров.
  7. В том же предложении шаблон проектирования уделяет больше внимания идее, и нет необходимости копировать шаблон кода. И не используйте шаблоны проектирования везде, а используйте их только тогда, когда они вам действительно нужны для повышения масштабируемости нашего кода.

Эта статья является третьей статьей журнала "Шаблоны проектирования", в которой в основном рассказывается о шаблонах проектирования, улучшающих масштабируемость. Остальные три статьи:

(480 лайков!) Не умеете инкапсулировать код? Взгляните на эти шаблоны дизайна!

Не знаете, как улучшить возможность повторного использования кода? Ознакомьтесь с этими шаблонами проектирования

Не знаете, как улучшить качество кода? Взгляните на эти шаблоны дизайна!

есть еще один提高代码质量шаблон дизайна.

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

Материал этой статьи взят изNetEase Senior Front-end Engineer Development Micro MajorКурс по шаблонам дизайна от Тан Лея.

Добро пожаловать, чтобы обратить внимание на мой общедоступный номербольшой фронт атакиПолучите высококачественные оригиналы впервые~

Цикл статей "Передовые передовые знания":nuggets.capable/post/684490…

Адрес GitHub с исходным кодом из серии статей «Advanced Front-end Knowledge»:GitHub.com/Денис — см....