Эта статья была рецензирована Джеффом Моттом . Спасибо всем рецензентам SitePoint за то, что сделали контент SitePoint как можно лучше!
Вы не можете далеко продвинуться как программист JavaScript, не узнав о функциях и объектах, и когда они используются вместе, они являются строительными блоками, которые нам нужны, чтобы начать работу с мощной парадигмой объекта, называемой составом . Сегодня мы рассмотрим некоторые идиоматические шаблоны для использования фабричных функций для составления функций, объектов и обещаний.
Когда функция возвращает объект, мы называем его фабричной функцией .
Давайте посмотрим на простой пример.
function createJelly() { return { type: 'jelly', colour: 'red' scoops: 3 }; }
Каждый раз, когда мы вызываем эту фабрику, она возвращает новый экземпляр объекта jelly.
Важно отметить, что нам не нужно ставить префикс перед именами фабрики с помощью create
но это может сделать назначение функции более понятным для других. То же самое верно для свойства type
но часто оно может помочь нам различать объекты, проходящие через наши программы.
Параметризованные фабричные функции
Как и все функции, мы можем определить нашу фабрику с параметрами, которые изменяют форму возвращаемого объекта.
function createIceCream(flavour='Vanilla') { return { type: 'icecream', scoops: 3, flavour } }
Теоретически, вы можете использовать параметризованные фабрики с сотнями аргументов, чтобы возвращать очень специфические и глубоко вложенные объекты, но, как мы увидим, это совсем не в духе композиции.
Композиционные фабричные функции
Определение одной фабрики с точки зрения другой помогает нам разбить сложные фабрики на более мелкие, многократно используемые фрагменты.
Например, мы можем создать фабрику по производству десертов, которая определяется ранее как фабрики по производству желе и мороженого.
function createDessert() { return { type: 'dessert', bowl: [ createJelly(), createIceCream() ] }; }
Мы можем создавать фабрики для создания сколь угодно сложных объектов, которые не требуют от нас возиться с новым или этим .
Объекты, которые могут быть выражены в терминах отношения « есть» , а не « есть», могут быть реализованы с помощью композиции, а не наследования.
Например, с наследованием.
// A trifle *is a* dessert function Trifle() { Dessert.apply(this, arguments); } Trifle.prototype = Dessert.prototype; // or class Trifle extends Dessert { constructor() { super(); } }
Мы можем выразить ту же идею с композицией.
// A trifle *has* layers of jelly, custard and cream. It also *has a* topping. function createTrifle() { return { type: 'trifle', layers: [ createJelly(), createCustard(), createCream() ], topping: createAlmonds() }; }
Асинхронные фабричные функции
Не все фабрики будут готовы немедленно вернуть данные. Например, некоторым придется сначала получить данные.
В этих случаях мы можем определить фабрики, которые вместо этого возвращают обещания.
function getMeal(menuUrl) { return new Promise((resolve, reject) => { fetch(menuUrl) .then(result => { resolve({ type: 'meal', courses: result.json() }); }) .catch(reject); }); }
Этот вид глубоко вложенных отступов может затруднить чтение и тестирование асинхронных фабрик. Часто бывает полезно разбить их на несколько отдельных фабрик, а затем составить их.
function getMeal(menuUrl) { return fetch(menuUrl) .then(result => result.json()) .then(json => createMeal(json)); } function createMeal(courses=[]) { return { type: 'meal', courses }; }
Конечно, мы могли бы использовать обратные вызовы вместо этого, но у нас уже есть инструменты, такие как Promise.all
для создания фабрик, которые возвращают обещания.
function getWeeksMeals() { const menuUrl = 'jsfood.com/'; return Promise.all([ getMeal(`${menuUrl}/monday`), getMeal(`${menuUrl}/tuesday`), getMeal(`${menuUrl}/wednesday`), getMeal(`${menuUrl}/thursday`), getMeal(`${menuUrl}/friday`) ]); }
Мы используем get
вместо create
в качестве соглашения об именах, чтобы показать, что эти фабрики выполняют некоторую асинхронную работу и возвращают обещания.
Функции и методы
До сих пор мы не видели никаких фабрик, которые возвращают объекты с методами, и это намеренно. Это потому, что, как правило, нам не нужно .
Фабрики позволяют нам отделять наши данные от наших вычислений.
Это означает, что мы всегда сможем сериализовать наши объекты как JSON, что важно для сохранения их между сеансами, отправки их через HTTP или WebSockets и помещения их в хранилища данных.
Например, вместо определения метода eat для объектов jelly, мы можем просто определить новую функцию, которая принимает объект в качестве параметра и возвращает измененную версию.
function eatJelly(jelly) { if(jelly.scoops > 0) { jelly.scoops -= 1; } return jelly; }
Небольшая синтаксическая справка делает эту модель жизнеспособной для тех, кто предпочитает программировать без изменения структур данных.
function eat(jelly) { if(jelly.scoops > 0) { return { ...jelly, scoops: jelly.scoops - 1 }; } else { return jelly; } }
Теперь вместо того, чтобы писать:
import { createJelly } from './jelly'; createJelly().eat();
Ну пиши:
import { createJelly, eatJelly } from './jelly'; eatJelly(createJelly());
Конечным результатом является функция, которая принимает объект и возвращает объект.
И что мы называем функцией, которая возвращает объект? Завод!
Фабрики высшего порядка
Передача фабрики в качестве функций более высокого порядка дает нам огромный контроль. Например, мы можем использовать эту концепцию для создания энхансеров .
function giveTimestamp(factory) { return (...args) => { const instance = factory(...args); const time = Date.now(); return { time, instance }; }; } const createOrder = giveTimestamp(function(ingredients) { return { type: 'order', ingredients }; });
Этот энхансер берет существующую фабрику и упаковывает ее, чтобы создать фабрику, которая возвращает экземпляры с временными метками.
В качестве альтернативы, если мы хотим, чтобы фабрика возвращала неизменные объекты, мы могли бы улучшить ее с помощью морозильника .
function freezer(factory) { return (...args) => Object.freeze(factory(...args))); } const createImmutableIceCream = freezer(createIceCream); createImmutableIceCream('strawberry').flavour = 'mint'; // Error!
Вывод
Как сказал однажды мудрый программист :
Гораздо легче восстановиться без абстракции, чем с неправильной абстракцией.
Проекты JavaScript имеют тенденцию усложняться тестированием и рефакторингом из-за запутанных уровней абстракции, которые мы часто поощряем создавать.
Прототипы и классы реализуют простую идею с помощью сложных и неестественных инструментов, таких как new
и this
до сих пор вызывает все виды путаницы даже сейчас — спустя годы после их добавления в язык.
Объекты и функции имеют смысл для программистов из большинства областей, и оба они являются примитивными типами в JavaScript, поэтому можно утверждать, что фабрики вообще не являются абстракцией!
Использование этих простых строительных блоков делает наш код более дружественным для неопытных программистов, и это определенно то, о чем мы все должны заботиться. Фабрики побуждают нас моделировать сложные и асинхронные данные с помощью примитивов, которые имеют естественную способность к компоновке, не вынуждая нас также достигать абстракций высокого уровня. JavaScript слаще, когда мы придерживаемся простоты!