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

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

разговаривать扩展性Новая статья о шаблонах проектированияШаблоны проектирования, используемые в исходном коде фреймворка для улучшения расширяемости.Он был выпущен, добро пожаловать в чтение ~

Зачем инкапсулировать код?

Мы часто слышим: «Написание кода требует хорошей инкапсуляции, высокой связности и низкой связанности». Так что же считается хорошей инкапсуляцией и почему мы ее инкапсулируем? На самом деле упаковка имеет ряд преимуществ:

  1. Инкапсулированный код, внутренние переменные не будут загрязнять внешний.
  2. Может вызываться извне как модуль. Внешнему вызывающему объекту не нужно знать подробности реализации, ему нужно только использовать ее в соответствии с согласованной спецификацией.
  3. Открытый для расширения, закрытый для модификации, то есть принцип открытого-закрытого. Модуль нельзя модифицировать извне, что не только обеспечивает корректность работы модуля, но и позволяет гибко использовать расширенные интерфейсы.

Как упаковать код?

В экосистеме JS уже есть много модулей, некоторые из которых очень хорошо упакованы и просты в использовании, например, jQuery, Vue и т. д. Если мы внимательно посмотрим на исходный код этих модулей, то обнаружим, что их инкапсуляция обычная. Эти правила обобщены как шаблоны проектирования.К шаблонам проектирования, используемым для инкапсуляции кода, в основном относятся:工厂模式,创建者模式,单例模式,原型模式четыре. Давайте взглянем на эти четыре шаблона проектирования в сочетании с исходным кодом фреймворка:

заводской узор

Название фабричного шаблона очень простое, а инкапсулированный модуль похож на фабрику, которая группирует необходимые объекты. Особенностью общего фабричного шаблона является то, что его не нужно использовать при вызовеnewИ входящие параметры относительно просты. Но количество вызовов может быть более частым, часто нужно выводить разные объекты, когда не частые вызовыnewТакже намного удобнее. Структура кода фабричного шаблона выглядит следующим образом:

function factory(type) {
  switch(type) {
    case 'type1':
      return new Type1();
    case 'type2':
      return new Type2();
    case 'type3':
      return new Type3();
  }
}

В приведенном выше коде мы передалиtype, а затем фабрика меняется в зависимости отtypeдля создания различных объектов.

Пример: всплывающий компонент

Давайте рассмотрим пример использования шаблона factory, если у нас есть следующие требования:

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

Для таких всплывающих окон давайте сначала создадим класс:

function infoPopup(content, color) {}
function confirmPopup(content, color) {}
function cancelPopup(content, color) {}

Если мы используем эти классы напрямую, это выглядит так:

let infoPopup1 = new infoPopup(content, color);
let infoPopup2 = new infoPopup(content, color);
let confirmPopup1 = new confirmPopup(content, color);
...

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

// 新加一个方法popup把这几个类都包装起来
function popup(type, content, color) {
  switch(type) {
    case 'infoPopup':
      return new infoPopup(content, color);
    case 'confirmPopup':
      return new confirmPopup(content, color);
    case 'cancelPopup':
      return new cancelPopup(content, color);
  }
}

Затем мы используемpopupне нужноnewЕсли вы вызываете функцию напрямую:

let infoPopup1 = popup('infoPopup', content, color); 

объектно-ориентированный

Хотя приведенный выше код реализует фабричный шаблон, ноswitchЭто не всегда выглядит очень элегантно. Мы используем объектно-ориентированное преобразованиеpopup, измените его на класс и монтируйте различные типы всплывающих окон в этом классе как фабричные методы:

function popup(type, content, color) {
  // 如果是通过new调用的,返回对应类型的弹窗
  if(this instanceof popup) {
    return new this[type](content, color);
  } else {
    // 如果不是new调用的,使用new调用,会走到上面那行代码
    return new popup(type, content, color);
  }
}

// 各种类型的弹窗全部挂载在原型上成为实例方法
popup.prototype.infoPopup = function(content, color) {}
popup.prototype.confirmPopup = function(content, color) {}
popup.prototype.cancelPopup = function(content, color) {}

упакован в модули

этоpopupДавайте просто одним временем звонкаnew, он фактически инкапсулировал в него различные связанные всплывающие окна, этоpopupМожет использоваться непосредственно как модульexportВыходите, чтобы позвонить другим, вы также можете установить его вwindowКак модуль его могут вызывать другие. потому чтоpopupОн инкапсулирует различные детали всплывающего окна даже послеpopupЕсли изменить внутренности, или добавить тип всплывающего окна, или изменить имя класса всплывающего окна, пока параметры внешнего интерфейса остаются неизменными, это не повлияет на внешнее. Прикреплено кwindowПоскольку модуль может использовать функции самореализации:

(function(){
 	function popup(type, content, color) {
    if(this instanceof popup) {
      return new this[type](content, color);
    } else {
      return new popup(type, content, color);
    }
  }

  popup.prototype.infoPopup = function(content, color) {}
  popup.prototype.confirmPopup = function(content, color) {}
  popup.prototype.cancelPopup = function(content, color) {}
  
  window.popup = popup;
})()

// 外面就直接可以使用popup模块了
let infoPopup1 = popup('infoPopup', content, color); 

Фабричный шаблон jQuery

jQuery также является типичным фабричным шаблоном: вы указываете ему параметр, и он возвращает вам DOM-объект, соответствующий этому параметру. Этот jQuery не нуженnewКак реализован фабричный паттерн? На самом деле jQuery звонит вам изнутри.newВот и все, процесс вызова jQuery упрощается следующим образом:

(function(){
  var jQuery = function(selector) {
    return new jQuery.fn.init(selector);   // new一下init, init才是真正的构造函数
  }

  jQuery.fn = jQuery.prototype;     // jQuery.fn就是jQuery.prototype的简写

  jQuery.fn.init = function(selector) {
    // 这里面实现真正的构造函数
  }

  // 让init和jQuery的原型指向同一个对象,便于挂载实例方法
  jQuery.fn.init.prototype = jQuery.fn;  

  // 最后将jQuery挂载到window上
  window.$ = window.jQuery = jQuery;
})();

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

var jQuery = function(selector) {
  if(!(this instanceof jQuery)) {
    return new jQuery(selector);
  }
  
  // 下面进行真正构造函数的执行
}

Приведенный выше код намного проще, и его также можно реализовать безnewЗвоните напрямую, здесь используются следующие функции:thisв функцииnewПри вызове указывает наnewвне объекта,newОбъект, который выходит, естественно, является классомinstance,здесьthis instanceof jQueryто естьtrue. Если это обычный звонок, онfalse, мы поможем емуnewодин раз.

режим строителя

Шаблон построителя используется для построения более сложных крупных объектов, таких какVue,VueОн содержит мощный и логически сложный объект, и при построении необходимо передать множество параметров. Таких ситуаций, которые нужно создавать, не так много, и шаблон построителя применим, когда сам создаваемый объект очень сложен. Общая структура шаблона строителя выглядит следующим образом:

function Model1() {}   // 模块1
function Model2() {}   // 模块2

// 最终使用的类
function Final() {
  this.model1 = new Model1();
  this.model2 = new Model2();
}

// 使用时
var obj = new Final();

В приведенном выше коде мы используемFinal,ноFinalСтруктура внутри более сложная, много подмодулей,FinalЭто объединение этих подмодулей для завершения функции.Такая тонкая структура подходит для режима строителя.

Пример: плагин редактора

Допустим, у нас есть такое требование:

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

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

  1. Самому редактору обязательно нужен класс, представляющий собой интерфейс для внешних вызовов
  2. Требуется класс, управляющий инициализацией параметров и отрисовкой страницы.
  3. Нужен класс для управления шрифтом
  4. Нужен класс для управления состоянием
// 编辑器本身,对外暴露
function Editor() {
  // 编辑器里面就是将各个模块组合起来实现功能
  this.initer = new HtmlInit();
  this.fontController = new FontController();
  this.stateController = new StateController(this.fontController);
}

// 初始化参数,渲染页面
function HtmlInit() {
  
}
HtmlInit.prototype.initStyle = function() {}     // 初始化样式
HtmlInit.prototype.renderDom = function() {}     // 渲染DOM

// 字体控制器
function FontController() {
  
}
FontController.prototype.changeFontColor = function() {}    // 改变字体颜色
FontController.prototype.changeFontSize = function() {}     // 改变字体大小

// 状态控制器
function StateController(fontController) {
  this.states = [];       // 一个数组,存储所有状态
  this.currentState = 0;  // 一个指针,指向当前状态
  this.fontController = fontController;    // 将字体管理器注入,便于改变状态的时候改变字体
}
StateController.prototype.saveState = function() {}     // 保存状态
StateController.prototype.backState = function() {}     // 后退状态
StateController.prototype.forwardState = function() {}     // 前进状态

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

StateController.prototype.backState = function() {
  var state = this.states[this.currentState - 1];  // 取出上一个状态
  this.fontController.changeFontColor(state.color);  // 改回上次颜色
  this.fontController.changeFontSize(state.size);    // 改回上次大小
}

одноэлементный шаблон

Одноэлементный шаблон подходит для сценариев, в которых глобально существует только один объект-экземпляр.Общая структура одноэлементного шаблона выглядит следующим образом:

function Singleton() {}

Singleton.getInstance = function() {
  if(this.instance) {
    return this.instance;
  }
  
  this.instance = new Singleton();
  return this.instance;
}

В приведенном выше кодеSingletonКласс монтирует статический методgetInstance, если вы хотите получить объект-экземпляр, вы можете получить его только с помощью этого метода. Этот метод определит, существует ли существующий объект-экземпляр. Если есть, он вернет, а если нет, создаст новый.

Пример: объект глобального хранилища данных

Допустим, теперь у нас есть такое требование:

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

Это требование требует, чтобы в глобальном масштабе существовал только один объект хранения данных, что является типичным сценарием, подходящим для одноэлементного режима Мы можем напрямую применить приведенный выше шаблон кода, но приведенный выше шаблон кода получаетinstanceдолжны быть скорректированыgetInstanceТолько, если пользователь напрямую звонитSingleton()илиnew Singleton()Будут проблемы, на этот раз меняем способ написания, чтобы он был совместимSingleton()а такжеnew Singleton(), что более глупо использовать:

function store() {
  if(store.instance) {
    return store.instance;
  }
  
  store.instance = this;
}

Приведенный выше код поддерживает использованиеnew store()метод, мы используем статическую переменнуюinstanceЧтобы записать, был ли он создан, если он был создан, верните этот экземпляр, если нет создания экземпляра, указывающего, что это первый вызов, затем поместитеthisНазначьте это этой статической переменной, потому что она используетсяnewпозвони, на этот разthisУказывает на экземпляр объекта, и, наконец, возвращает неявноthis.

Если мы все еще хотим поддерживатьstore()Вызовите напрямую, мы можем использовать метод, использованный в предыдущем фабричном шаблоне, для обнаруженияthisЯвляется ли это экземпляром текущего класса, если нет, используйте его для негоnewПросто позвони:

function store() {
  // 加一个instanceof检测
  if(!(this instanceof store)) {
    return new store();
  }
  
  // 下面跟前面一样的
  if(store.instance) {
    return store.instance;
  }
  
  store.instance = this;
}

Затем мы вызываем его двумя способами для обнаружения:

image-20200521154322364

Пример: vue-маршрутизатор

vue-routerНа самом деле также используется одноэлементный режим, поскольку наличие на странице нескольких объектов маршрутизации может привести к конфликту состояний.vue-routerРеализация синглтона немного отличается,Следующий код взят изvue-routerисходный код:

let _Vue;

function install(Vue) {
  if (install.installed && _Vue === Vue) return;
  install.installed = true

  _Vue = Vue
}

каждый раз, когда мы звонимvue.use(vueRouter)будет фактически выполняться, когдаvue-routerмодульныйinstallметод, если пользователь случайно вызывает его несколько разvue.use(vueRouter)вызоветinstallвыполняется несколько раз, что приводит к неверным результатам.vue-routerизinstallПри первом исполнении,installedсвойство записывается какtrue, и записывает текущийVue, так что после того жеVueвыполнить снова внутриinstallбудет напрямуюreturnДа, это тоже одноэлементный паттерн.

Можно видеть, что все три кода, которые у нас есть, являются одноэлементными режимами. Хотя они имеют разные формы, основная идея одна и та же. Все они используют переменную, чтобы отметить, был ли код выполнен. Если он выполнен, он будет вернуться к последнему времени. Это гарантирует, что несколько вызовов получат тот же результат.

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

Наиболее типичным применением режима прототипа является сам JS, а цепочка прототипов JS — это режим прототипа. можно использовать в JSObject.createУкажите объект в качестве прототипа для создания объекта:

const obj = {
  x: 1,
  func: () => {}
}

// 以obj为原型创建一个新对象
const newObj = Object.create(obj);

console.log(newObj.__proto__ === obj);    // true
console.log(newObj.x);    // 1

В приведенном выше коде мы будемobjв качестве прототипа, затем используйтеObject.createСозданные новые объекты будут иметь свойства и методы на этом объекте, что на самом деле является узором прототипа. И объектно-ориентированный js js на самом деле является воплощением этой картины. Например, наследование JS можно записать так:

function Parent() {
  this.parentAge = 50;
}
function Child() {}

Child.prototype = new Parent();
Child.prototype.constructor = Child;      // 注意重置constructor

const obj = new Child();
console.log(obj.parentAge);    // 50

Наследование здесь фактически позволяет подклассамChild.prototype.__proto__родительского классаprototype, чтобы получить методы и свойства родительского класса.В JS больше объектно-ориентированного контента, я не буду его здесь распространять, есть статья посвященная этому вопросу.

Суммировать

  1. Многие простые в использовании библиотеки с открытым исходным кодом имеют хорошую инкапсуляцию.Инкапсуляция может изолировать внутреннюю среду от внешней среды, облегчая внешнее использование.
  2. Существуют разные пакеты для разных сценариев.
  3. Компоненты, для которых требуется большое количество одинаковых экземпляров, могут быть упакованы в заводском режиме.
  4. Внутренняя логика более сложная, и для внешнего использования требуется не так много экземпляров, поэтому вы можете рассмотреть возможность использования шаблона построителя для ее инкапсуляции.
  5. Только один экземпляр глобального объекта должен быть инкапсулирован шаблоном singleton.
  6. Если между новыми и старыми объектами могут быть отношения наследования, вы можете рассмотреть возможность использования шаблона прототипа для инкапсуляции.JS сам по себе является типичным шаблоном прототипа.
  7. При использовании паттернов проектирования не копируйте шаблоны кода и, что более важно, осваивайте идею, ведь один и тот же паттерн может иметь разные схемы реализации в разных сценариях.

Эта статья является первой статьей о структуре проектирования, и есть еще три статьи. Добро пожаловать на чтение:

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

Основной материал статьи взят изNetEase Senior Front-end Engineer Development Micro MajorВидеокурс режима дизайна учителя Тан Лея, курс NetEase по-прежнему хорош, я рекомендую его всем.

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

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

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