исходный адрес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Очень хорошо сказано:
в JavaScript
new
а также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.
Мой паттерн 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Была очень хорошая дискуссия.
Наконец, я хотел бы познакомить вас с некоторыми личными привычками, которые я использовал в этих примерах кода:
- Я использую функциональные операторы вместо функциональных выражений
- Я поместил оператор возврата функции вверху функции (это стало возможным благодаря приведенному выше закону)
- Я называю нашу фабричную функцию как
makeX
, вместоcreateX
илиbuildX
или другой. - Моя фабричная функция использует один деструктурирующий объект в качестве параметра
- Я не использую запястью (Крокфорд также не поддерживает использование точки с запятой.)
- а также...
Вам могут нравиться другие стили кодирования, и это нормально Стили не являются шаблонами проектирования, и их не нужно строго соблюдать.
Здесь, я думаю, мы ясно поняли, что определение замороженного заводского режима таково:Используйте функцию для создания и возврата неизменяемого объекта, Как именно написать эту функцию, зависит от вас.
Если вы найдете эту статью очень полезной, пожалуйста, подпишитесь на нее и добавьте ее в закладки, а также перешлите ее своим друзьям, чтобы они тоже могли понять.