[Перевод] Элегантные современные шаблоны дизайна JavaScript: замороженная фабрика

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

исходный адресElegant patterns in modern JavaScript: Ice Factory

С конца 1990-х я время от времени занимался разработкой JavaScript. Сначала мне это не нравилось, но с тех пор, как я узнал о ES2015 (также называемом ES6), я начал думать, что JavaScript — это мощная и выдающаяся динамическая Язык программирования.

Со временем я освоил несколько паттернов кодирования, которые делают код более лаконичным, тестируемым и выразительным, и теперь я поделюсь этими паттернами с вами.

Первый шаблон, который я представил, былRORO(будет переведено позже) Не волнуйтесь, если вы не читали ее, потому что это не повлияет на чтение этой статьи, и вы можете прочитать ее в другой раз.

Сегодня я познакомлю вас сзамороженная фабрикамодель.

Фабрика заморозки — это просто функция, которая создает и возвращает неизменяемый объект Мы объясним это определение позже, но сначала давайте посмотрим, почему этот шаблон так полезен.

Классы JavaScript не идеальны.

Обычно мы объединяем некоторые связанные функции в объекте. Например, в приложении электронной коммерции у нас может бытьcartобъект, который раскрываетaddProductа такжеremoveProductдве функции. Мы можем передатьcart.addProduct()так же какcart.removeProduct()позвонить им.

Если вы когда-либо писали на объектно-ориентированном языке, ориентированном на классы, таком как Java или C#, это может показаться вам очень знакомым.

Если вы новичок, это не имеет значения, теперь вы уже видели.cart.addProduct()Это заявление Лично у меня есть оговорки по поводу такого способа написания.

Как мы создаем хорошийcartА как насчет объектов?Первой интуицией, связанной с JavaScript сегодня, должно быть использованиеclass, выглядит так:

// ShoppingCart.js
export default class ShoppingCart {
  constructor({db}) {
    this.db = db
  }
  
  addProduct (product) {
    this.db.push(product)
  }
  
  empty () {
    this.db = []
  }
  get products () {
    return Object
      .freeze([...this.db])
  }
  removeProduct (id) {
    // remove a product 
  }
  // other methods
}
// someOtherModule.js
const db = [] 
const cart = new ShoppingCart({db})
cart.addProduct({ 
  name: 'foo', 
  price: 9.99
})

Примечание. Для простоты я использую массив в качестве базы данных.db, В реальном коде это должно быть что-то вродеModelилиRepoЭти объекты могут взаимодействовать с реальной базой данных.

К сожалению, хотя этот код выглядит великолепно, в JavaScriptclassможет вести себя иначе, чем вы думаете.

Если вы не будете осторожны, JavaScript укусит вас в ответ.

Например, поnewОбъект, созданный с помощью ключевого слова, можно изменить, поэтому вы можетепереназначить

const db = []
const cart = new ShoppingCart({db})
cart.addProduct = () => 'nope!' 
// No Error on the line above!
cart.addProduct({ 
  name: 'foo', 
  price: 9.99
}) // output: "nope!" FTW?

Еще хуже,newСозданы объекты, которые наследуются от этогоclassизprototypeСледовательно, изменение прототипа этого класса повлияет на все объекты, созданные с помощью этого класса, даже если изменение будет выполнено после создания объекта.

Посмотрите этот пример:

const cart = new ShoppingCart({db: []})
const other = new ShoppingCart({db: []})
ShoppingCart.prototype
  .addProduct = () => ‘nope!’
// No Error on the line above!
cart.addProduct({ 
  name: 'foo', 
  price: 9.99
}) // output: "nope!"
other.addProduct({ 
  name: 'bar', 
  price: 8.88
}) // output: "nope!"

На самом деле, в JavaScriptthisдинамически связан. Если мы положимcartМетод объекта передается, что приведет к потереthisцитата Это очень нелогично и вызывает много проблем,

Распространенная ошибка — это когда мы привязываем метод экземпляра к обработчику событий. с нашимcart.emptyметод в качестве примера.

empty () {
    this.db = []
  }

Если мы напрямую привяжем этот метод к событию нажатия кнопки на нашей странице...

<button id="empty">
  Empty cart
</button>
document
  .querySelector('#empty')
  .addEventListener(
    'click', 
    cart.empty
  )

когда пользователь нажимает на этоemptyКогда кнопка нажата, их корзина все еще полна и не опустошена.

Этот провал молчит, потому чтоthisбудет указывать на этоbutton, вместо того, чтобы указывать наcart, поэтому нашcart.emptyметод, наконец, дастbuttonСоздать новое свойствоdbИ назначен[]вместо того, чтобы затрагиватьcartв объектеdb.

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

Чтобы заставить его работать правильно, мы можем сделать это:

document
  .querySelector("#empty")
  .addEventListener(
    "click", 
    () => cart.empty()
  )

я думаюMattias Petter JohanssonОчень хорошо сказано:

в JavaScriptnewа такжеthisИногда нелогичное, странное, как радужная ловушка

Режим Frozen Factory, чтобы спасти вас

Как я сказал ранее,Ледяная фабрика — это функция, которая создает и возвращает неизменяемый объект.Используя шаблон фабрики льда, наш пример корзины покупок переписывается следующим образом:

// makeShoppingCart.js
export default function makeShoppingCart({
  db
}) {
  return Object.freeze({
    addProduct,
    empty,
    getProducts,
    removeProduct,
    // others
  })
  function addProduct (product) {
    db.push(product)
  }
  
  function empty () {
    db = []
  }
  function getProducts () {
    return Object
      .freeze([...db])
  }
  function removeProduct (id) {
    // remove a product
  }
  // other functions
}
// someOtherModule.js
const db = []
const cart = makeShoppingCart({ db })
cart.addProduct({ 
  name: 'foo', 
  price: 9.99
})

Стоит отметить, что наша странная радужная ловушка исчезла:

  • нам больше не нужноnewМы просто вызываем обычную функцию JavaScript для создания нашегоcartобъект.

  • нам больше не нужноthisНаши функции-члены имеют прямой доступ кdbобъект.

  • нашcartОбъекты полностью неизменяемы.Object.freeze()замороженныйcartобъект, поэтому вы не можете добавлять к нему новые свойства, изменять или удалять существующие свойства, а цепочка прототипов не может быть изменена.Object.freeze()неглубокий, поэтому, если возвращаемый нами объект содержит массивы или другие объекты, мы должны убедиться, чтоObject.freeze()также влияет на них. Опять же, мы используемES模块Это также неизменяемо.Вам нужно использовать строгий режим, чтобы повторное назначение не вызывало ошибку, а не молчало.

Конфиденциальность

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

function makeThing(spec) {
  const secret = 'shhh!'
  return Object.freeze({
    doStuff
  })
  function doStuff () {
    // 我们可以在这里使用 spec 和 secret变量
  }
}
// secret 在这里无法被访问
const thing = makeThing()
thing.secret // undefined

JavaScript использует замыкания для выполнения этой функции, соответствующую информацию вы можете найти вMDNзапрос выше.

принятый закон

Несмотря на то, что паттерн factory уже давно используется в JavaScript, шаблон фабрики льда по-прежнему настоятельно рекомендуется.Douglas Crockfordв этотвидеоСоответствующий код показан в (видео требует научного доступа в Интернет).

Это код, продемонстрированный Крокфордом, он назвал функцию, которая создает объект, какconstructor.

Douglas Crockford 演示的代码启发了我

Мой паттерн Frozen Factory на примере Крокфорда, код выглядит так.

function makeSomething({ member }) {
  const { other } = makeSomethingElse() 
  
  return Object.freeze({ 
    other,
    method
  }) 
  function method () {
    // code that uses "member"
  }
}

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

я тоже поставилspecПараметры деконструируются, и шаблон переименовывается взамороженная фабрика, это имя удобнее запомнить, а также предотвращает и ES6constructorзапутался Но на самом деле это одно и то же.

Поэтому я искренне благодарю вас, мистер Крокфорд.

Примечание. Здесь стоит упомянуть, что Крокфорд считает, что перенос функций в переменную является злом JavaScript, и поэтому может считать эту версию неверной.эта статьяРассказал о своем понимании, более подробно, вэтот обзорсередина.

Как насчет наследства?

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

Наряду с нашим объектом корзины у нас может бытькатегорияобъект иЗаказВсе эти объекты могут предоставлять различные версииaddProductа такжеremoveProductфункция.

Мы все знаем, что дублирование кода — это плохое поведение, поэтому мы можем закончить тем, что попытаемся создать что-то вродеСписок продуктовобъект, нашкорзина, категориятак же какЗаказВсе объекты наследуются от него.

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

«Предпочитайте композицию объектов наследованию классов». – Шаблоны проектирования: элементы многоразового объектно-ориентированного программного обеспечения.

Мы должны использовать композицию объектов больше, чем наследование — шаблоны проектирования

Вот ссылка на книгуШаблоны проектирования

Фактически, автор этой книги, один из тех, кого мы обычно называем Бандой четырех, также сказал

«…наш опыт показывает, что дизайнеры злоупотребляют наследованием как методом повторного использования, а дизайны часто делают более пригодными для повторного использования (и проще) за счет большей зависимости от состава объекта». Наш опыт показывает, что программисты чрезмерно используют наследование как средство повторного использования, но проектирование с помощью шаблона композиции объектов сделает повторное использование более обширным и простым.

Поэтому нашСписок продуктовЗавод будет таким:

function makeProductList({ productDb }) {
  return Object.freeze({
    addProduct,
    empty,
    getProducts,
    removeProduct,
    // others
  )}
 
  // addProduct 以及其他函数的定义…
}

Затем нашкорзинаФабрика будет выглядеть так:

function makeShoppingCart(productList) {
  return Object.freeze({
    items: productList,
    someCartSpecificMethod,
    // …
)}
function someCartSpecificMethod () {
  // code 
  }
}

Затем мы можем передать список товаров в нашу корзину следующим образом:

const productDb = []
const productList = makeProductList({ productDb })
const cart = makeShoppingCart(productList)

мы сможем пройтиitemsсвойства для использованияproductList.Следующим образом:

cart.items.addProduct()

Мы также можем попытаться объединить весьproductListобъект в нашу корзину объект. Вот так

function makeShoppingCart({ 
  addProduct,
  empty,
  getProducts,
  removeProduct,
  …others
}) {
  return Object.freeze({
    addProduct,
    empty,
    getProducts,
    removeProduct,
    someOtherMethod,
    …others
)}
function someOtherMethod () {
  // code 
  }
}

Собственно, в более ранней версии этого поста я так и делал.Но потом узнал, что это немного опасно(здесьЕсть сопутствующие пояснения), поэтому их лучше объединить с помощью свойств объекта.

Отлично, я передал вам свои мысли

小心

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

Но по мере того, как мои часы растут, я все больше и больше понимаю, что нетВ конце концова такженикогда.Тольковыберитеа такжекомпромисс.

пройти череззамороженная фабрикаСпособ создания объектов будет быстрее обычного использованияclassПотребляйте больше памяти и снижайте производительность.

В примерах я описал выше, это не будет иметь значение, даже если они бегут быстрее, чемclassмедленный,замороженная фабрикаРежим по-прежнему очень быстрый.

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

Помните, сначала создайте приложение и не допускайте преждевременной оптимизации. В большинстве случаев создание объектов не является узким местом.

Хотя я жалуюсь здесь, ноclassЭто не всегда так уж плохо, вы не должны использовать его из-за фреймворка или библиотеки.classпросто отрицайте это.Dan Abramovоднажды в своей статьеHow to use Classes and Sleep at NightБыла очень хорошая дискуссия.

Наконец, я хотел бы познакомить вас с некоторыми личными привычками, которые я использовал в этих примерах кода:

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

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

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