Элегантные шаблоны в современном JavaScript: Ice Factory

1656660853 elegantnye shablony v sovremennom javascript ice factory

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

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

О первом узоре «РОРО» я писал в статье ниже. Не беспокойтесь, если вы не читали его, вы можете прочитать их в любом порядке.

Элегантные шаблоны в современном JavaScript: RORO
Я написал свои первые несколько строк JavaScript вскоре после того, как этот язык был изобретен. Если бы вы сказали мне, что я…medium.freecodecamp.org

Сегодня я хотел бы познакомить вас с узором «Фабрика льда».

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

Классы JavaScript не так классны

Часто имеет смысл сгруппировать связанные функции в один объект. К примеру, в программе электронной коммерции мы можем иметь cart объект, который раскрывает ан addProduct функция и a removeProduct функция. Затем мы могли бы вызвать эти функции с помощью cart.addProduct() и cart.removeProduct().

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

Если вы новичок в программировании — теперь, когда вы видели утверждение типа cart.addProduct(). Я подозреваю, что идея группировки функций под одним объектом выглядит достаточно хорошо.

Итак, как бы мы могли создать этих красивых малышей cart объект? Вашим первым инстинктом с современным JavaScript может быть использование a 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 параметр для простоты. В реальном коде это будет что-то вроде модели или репозитория, взаимодействующего с реальной базой данных.

К сожалению – даже если это выглядит красиво – классы в JavaScript ведут себя совсем иначе, чем вы могли бы ожидать.

Классы 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 ключевое слово наследовать prototype с class который использовался для их создания. Итак, изменения в классе prototype влиять все созданные из этого объекты class — даже если внесены изменения после объект создан!

Посмотри на это:

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!"

Тогда есть тот факт, что this В JavaScript динамически привязан. Итак, если мы обойдем методы нашего cart объект, ссылку на который мы можем потерять this. Это очень глупо, и это может создать для нас много проблем.

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

Рассмотрим наш cart.empty метод.

empty () {    this.db = []  }

Если мы назначим этот метод непосредственно к click событие кнопки на нашей веб-странице…

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

… когда пользователи щелкают пустое поле buttonих cart останется полным.

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

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

Чтобы это заработало, нам нужно сделать:

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

или:

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

Я думаю, что Маттиас Петтер Йоханссон сказал это лучше:

«new и this [in JavaScript] это какая-то неинтуитивная, странная ловушка облачной радуги».

Ice Factory в помощь

Как я сказал раньше, Ice Factory – это только функция, которая создает и возвращает замороженный объект. С Ice 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() есть неглубокийпоэтому если возвращаемый объект содержит array или другой object мы должны убедиться Object.freeze() их тоже. Кроме того, если вы используете замороженный объект вне модуля ES, вам нужно перейти в строгий режим, чтобы убедиться, что повторное назначение вызывает ошибку, а не просто молчаливый сбой.

Немного конфиденциальности, пожалуйста

Еще одно преимущество Ice Factories состоит в том, что они могут иметь частных участников. Например:

function makeThing(spec) {  const secret="shhh!"
  return Object.freeze({    doStuff  })
  function doStuff () {    // We can use both spec    // and secret in here   }}
// secret is not accessible out here
const thing = makeThing()thing.secret // undefined

Это стало возможным благодаря закрытиям JavaScript, о которых вы можете прочитать больше на MDN.

Пожалуйста, небольшое признание

Несмотря на то, что Factory Functions были вокруг JavaScript всегда, шаблон Ice Factory был в значительной степени вдохновлен некоторым кодом, который Дуглас Крокфорд показал в этом видео.

Вот Крокфорд демонстрирует создание объекта с помощью функции, которую он называет конструктором:

1*KHVORQsCaE5EJFCb9FsfGA
Дуглас Крокфорд демонстрирует вдохновивший меня код.

Моя версия Ice Factory примера Crockford выше будет выглядеть так:

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

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

Я также использовал деструктуризацию на spec параметр. И я переименовал узор в «Фабрика льда», чтобы он лучше запоминался и его было легче спутать с constructor функция с JavaScript class. Но в основном это тоже самое.

Благодарю вас, мистер Крокфорд.

Примечание: Вероятно, стоит упомянуть, что Крокфорд считает «подъем» функции «плохой частью» JavaScript и, скорее всего, сочтет мою версию ересью. Я обсуждал свои чувства на этот счет в предыдущей статье, а точнее, в этом комментарии.

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

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

Вместе с корзиной для покупок мы, вероятно, имеем объект Catalog и Order. И все это, вероятно, открывает определенные версии `addProduct` и `removeProduct`.

Мы знаем, что дублирование – это плохо, поэтому в конце концов у нас возникнет соблазн создать что-то вроде объекта «Список продуктов», который может унаследовать нашу корзину, каталог и заказ.

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

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

На самом деле, авторы этой книги — известной как «Банда четырех» — говорят:

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

Итак, вот наш список продуктов:

function makeProductList({ productDb }) {  return Object.freeze({    addProduct,    empty,    getProducts,    removeProduct,    // others  )}   // definitions for   // addProduct, etc…}

А вот и наша корзина:

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

И теперь мы можем просто добавить наш список продуктов в нашу корзину, вот так:

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

И используйте список продуктов через свойство `items`. Люблю:

cart.items.addProduct()

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

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

Фактически в предыдущей версии этой статьи я сделал именно это. Но потом мне указали, что это несколько опасно (как объясняется здесь). Итак, нам лучше придерживаться правильной композиции объектов.

потрясающе Я продан!

1*PfWy93k2QgidbBFVGySgwA
Осторожно

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

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

Создание объектов с помощью Ice Factory происходит более медленно и занимает больше памяти, чем использование класса.

В типах случаев использования, которые я описал, это не будет иметь значения. Несмотря на то, что они медленнее классов, Ice Factories все еще довольно быстры.

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

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

Несмотря на мои предварительные высказывания, занятия не всегда ужасны. Вы не должны выбрасывать структуру или библиотеку только потому, что они используют классы. На самом деле, Дэн Абрамов довольно красноречиво написал об этом в своей статье «Как использовать занятия и спать ночью».

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

Вы можете выбрать разные стили, и это нормально! Стиль – это не шаблон.

Шаблон Ice Factory просто: использовать функцию для создания и возврата замороженного объекта. Как вы запишете эту функцию, зависит от вас.

Если вы считаете статью полезной, пожалуйста, разбейте этот значок аплодисментов несколько раз, чтобы распространить информацию. И если вы хотите узнать больше подобных вещей, подпишитесь на мою рассылку Dev Mastery ниже. Спасибо!

ОБНОВЛЕНИЕ 2019: Вот видео, где я часто использую этот шаблон!

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *