Привет! Меня зовут Айнур, и я frontend‑разработчик SimbirSoft. Более 6 лет я работаю над коммерческими проектами, создаю и улучшаю интерфейсы, поэтому в работе достаточно часто использую паттерны проектирования. Неоднократно я обращался за идеями и лайфхаками к книге Patterns.dev, которая содержит очень современный взгляд на шаблоны проектирования, рендеринга и производительности JavaScript.
Авторы Patterns.dev:
Лидия Холли — штатный консультант и преподаватель по разработке программного обеспечения, которая в основном работает с JavaScript, React, Node, GraphQL. Она также занимается наставничеством и проводит личные тренинги.
Эдди Османи — технический менеджер, работающий над Google Chrome. Его команды работают над такими проектами, как Lighthouse, PageSpeed Insights, Chrome User Experience Report и другими.
Материал книги будет полезен не только React‑разработчикам, но и всем, кто так или иначе интересуется или сталкивается с frontend‑разработкой.
В этом посте я публикую свой перевод первой из трех частей книги. В ней речь идет о множестве различных паттернов, которые включает в себя современная веб‑разработка. Если материал будет вам полезен и получит положительный отклик, то я опубликую перевод следующей части.
Справочная информация:
Первая часть взята из книги: https://www.patterns.dev/, переведена на русский язык. Книга находится под лицензией CC BY-NC 4.0
Данный адаптированный материал распространяется на условиях лицензии Creative Commons Attribution-NonCommercial 4.0 International (CC BY-NC 4.0)
I. Design Patterns
Введение
Паттерны проектирования являются фундаментальной частью разработки программного обеспечения (далее — ПО), поскольку они обеспечивают типичные решения часто повторяющихся проблем при проектировании ПО. Вместо того чтобы предоставлять конкретные части ПО, паттерны проектирования — это всего лишь концепции, которые можно использовать для оптимального решения повторяющихся проблем.
За последние несколько лет экосистема веб‑разработки сильно изменилась. В то время как некоторые известные паттерны проектирования, возможно, уже не так ценны, как раньше, другие эволюционировали, чтобы решать современные проблемы с помощью новейших технологий.
JavaScript‑библиотека React от Facebook за последние 5 лет приобрела огромную популярность и в настоящее время является наиболее загружаемым фреймворком на NPM по сравнению с конкурирующими JavaScript‑библиотеками, такими как Angular, Vue, Ember и Svelte. Благодаря популярности React, паттерны проектирования были изменены, оптимизированы и созданы новые, чтобы обеспечить ценность в текущей современной экосистеме веб‑разработки. В последней версии React появилась новая функция под названием Hooks, которая играет очень важную роль в дизайне вашего приложения и может заменить многие традиционные паттерны проектирования.
Современная веб‑разработка включает в себя множество различных паттернов. В этом проекте рассматриваются реализация, преимущества и подводные камни распространенных паттернов проектирования с использованием ES2015+, специфические для React паттерны проектирования и их возможная модификация и реализация с использованием React Hooks, а также многие другие паттерны и оптимизации, которые могут помочь улучшить ваше современное веб‑приложение!
Паттерн Singleton («Синглтон»)
Совместное использование одного глобального экземпляра во всем нашем приложении.
Singleton — это класс, экземпляр, которого может быть создан один раз, а доступ к нему может быть глобальным. Этот единственный экземпляр может быть общим для всего нашего приложения, что делает Singleton отличным средством управления глобальным состоянием в приложении.
Сначала давайте посмотрим, как может выглядеть Singleton, на примере класса ES2015. Для этого примера мы создадим класс Counter, который имеет:
метод getInstance, который возвращает значение экземпляра;
метод getCount, который возвращает текущее значение переменной счетчика;
метод increment, который увеличивает значение счетчика на единицу;
метод decrement, который уменьшает значение счетчика на единицу.
![Источник изображения: https://www.patterns.dev/posts/singleton-pattern/ Источник изображения: https://www.patterns.dev/posts/singleton-pattern/](https://habrastorage.org/getpro/habr/upload_files/bae/5b9/871/bae5b98719cd0a6bbcb1d1c039f4f71e.png)
Однако этот класс не соответствует критериям Singleton! Singleton может быть создан только один раз. В настоящее время мы можем создать несколько экземпляров класса Counter.
![Источник изображения: https://www.patterns.dev/posts/singleton-pattern/ Источник изображения: https://www.patterns.dev/posts/singleton-pattern/](https://habrastorage.org/getpro/habr/upload_files/d89/fcf/a14/d89fcfa14412cc5e821a870f99b83eb7.png)
Вызвав новый метод дважды, мы просто установили counter1 и counter2, равными разным экземплярам. Значения, возвращенные методом getInstance для counter1 и counter2, фактически вернули ссылки на разные экземпляры: они не являются строго равными!
![Источник изображения: https://www.patterns.dev/posts/singleton-pattern/ Источник изображения: https://www.patterns.dev/posts/singleton-pattern/](https://habrastorage.org/getpro/habr/upload_files/a41/3dd/244/a413dd2446910dba15acb9d0887e2b1e.png)
Давайте убедимся, что может быть создан только один экземпляр класса Counter.
Один из способов убедиться в том, что может быть создан только один экземпляр — это создать переменную instance. В конструкторе счетчика мы можем установить instance равной ссылке на экземпляр, когда создается новый экземпляр. Мы можем предотвратить создание новых экземпляров, проверив, не имеет ли переменная instance уже значения. Если это так, то экземпляр уже существует.
Этого не должно произойти: у пользователя должна возникнуть ошибка.
![Источник изображения: https://www.patterns.dev/posts/singleton-pattern/ Источник изображения: https://www.patterns.dev/posts/singleton-pattern/](https://habrastorage.org/getpro/habr/upload_files/23e/c75/345/23ec7534518da3ad175a9131018f072b.png)
Отлично! Мы больше не можем создавать несколько экземпляров.
Давайте экспортируем экземпляр Counter из файла counter.js. Но прежде чем это сделать, мы должны заморозить экземпляр. Метод Object.freeze гарантирует, что потребляющий код не сможет изменить Singleton. Свойства замороженного экземпляра не могут быть добавлены или изменены, что снижает риск случайной перезаписи значений Singleton.
![Источник изображения: https://www.patterns.dev/posts/singleton-pattern/ Источник изображения: https://www.patterns.dev/posts/singleton-pattern/](https://habrastorage.org/getpro/habr/upload_files/d10/2a4/247/d102a42474006234d8d05e44776f5e61.png)
Давайте рассмотрим приложение, реализующее пример со счетчиком. У нас есть следующие файлы:
counter.js: содержит класс Counter и экспортирует экземпляр Counter в качестве экспорта по умолчанию;
index.js: загружает модули redButton.js и blueButton.js;
redButton.js: импортирует Counter, добавляет метод инкремента Counter в качестве слушателя событий на красную кнопку и регистрирует текущее значение счетчика, вызывая метод getCount;
blueButton.js: импортирует Counter, добавляет метод инкремента Counter в качестве слушателя событий для синей кнопки и регистрирует текущее значение счетчика, вызывая метод getCount.
И blueButton.js, и redButton.js импортируют один и тот же экземпляр из counter.js. Этот экземпляр импортируется как Counter в обоих файлах.
![Источник изображения: https://www.patterns.dev/posts/singleton-pattern/ Источник изображения: https://www.patterns.dev/posts/singleton-pattern/](https://habrastorage.org/getpro/habr/upload_files/683/8bc/bfd/6838bcbfdab3ae0cfff9efa5ebf1fb82.png)
Когда мы вызываем метод increment в файле redButton.js или blueButton.js, значение свойства counter экземпляра Counter обновляется в обоих файлах. Неважно, нажимаем ли мы на красную или синюю кнопку: одно и то же значение является общим для всех экземпляров. Вот почему счетчик продолжает увеличиваться на единицу, даже если мы вызываем метод в разных файлах.
Управление состоянием в React
В React мы часто полагаемся на глобальное состояние через инструменты управления состоянием, такие как Redux или React Context, вместо использования Singleton. Хотя поведение их глобального состояния может показаться похожим на поведение Singleton, эти инструменты предоставляют состояние только для чтения, а не изменяемое состояние Singleton. При использовании Redux только редьюсеры могут обновлять состояние, после того как компонент отправил действие (action) через dispatch.
Хотя, недостатки глобального состояния не исчезают волшебным образом при использовании этих инструментов, по крайней мере, мы можем быть уверены, что глобальное состояние изменяется так, как мы задумали, поскольку компоненты не могут обновлять состояние напрямую.
Паттерн Proxy
Перехват и управление взаимодействием с целевыми объектами.
С помощью прокси‑объекта мы получаем больше контроля над взаимодействием с определенными объектами. Прокси‑объект может переопределять поведение всякий раз, когда мы взаимодействуем с объектом, например, когда мы получаем значение или устанавливаем значение.
Вообще говоря, proxy означает представителя кого‑то другого (например, человека). Вместо того чтобы говорить с этим человеком напрямую, вы будете говорить с прокси‑персоной, которая будет представлять человека, с которым вы пытались связаться. То же самое происходит и в JavaScript: вместо того чтобы взаимодействовать с целевым объектом напрямую, мы будем взаимодействовать с прокси‑объектом.
Давайте создадим объект person, который представляет John Doe (прим. перев. — неизвестный).
![Источник изображения: https://www.patterns.dev/posts/proxy-pattern/ Источник изображения: https://www.patterns.dev/posts/proxy-pattern/](https://habrastorage.org/getpro/habr/upload_files/811/14e/142/81114e1421721026b3c8ee8c275f4299.png)
Вместо того чтобы взаимодействовать с этим объектом напрямую, мы хотим взаимодействовать с прокси‑объектом. В JavaScript мы можем легко создать новый прокси‑объект, создав новый экземпляр Proxy.
![Источник изображения: https://www.patterns.dev/posts/proxy-pattern/ Источник изображения: https://www.patterns.dev/posts/proxy-pattern/](https://habrastorage.org/getpro/habr/upload_files/087/847/1ab/0878471abacd6bcf884c4b5ff37b65a8.png)
Вторым аргументом Proxy является объект, представляющий обработчик. В объекте обработчика мы можем определить конкретное поведение в зависимости от типа взаимодействия. Хотя существует множество методов, которые вы можете добавить в обработчик Proxy, два наиболее распространенных из них — get и set:
get: вызывается при попытке получить доступ к свойству;
set: вызывается при попытке изменить свойство.
Эффективно, в конечном итоге будет происходить следующее:
![Источник изображения: https://www.patterns.dev/posts/proxy-pattern/ Источник изображения: https://www.patterns.dev/posts/proxy-pattern/](https://habrastorage.org/getpro/habr/upload_files/0a0/e8d/ecc/0a0e8deccd9785550f83a3284a85683a.png)
![Источник изображения: https://www.patterns.dev/posts/proxy-pattern/ Источник изображения: https://www.patterns.dev/posts/proxy-pattern/](https://habrastorage.org/getpro/habr/upload_files/a03/25c/0e8/a0325c0e87e7fb2ce60674a15efde8c5.png)
Вместо того чтобы взаимодействовать с объектом person напрямую, мы будем взаимодействовать с personProxy.
Давайте добавим обработчики к прокси personProxy. При попытке изменить свойство, вызывая метод set у прокси, мы хотим, чтобы прокси регистрировал предыдущее и новое значение свойства. При попытке получить доступ к свойству, вызывая метод get у прокси, мы хотим, чтобы прокси записывал в журнал более читаемое предложение, содержащее ключ и значение свойства.
![Источник изображения: https://www.patterns.dev/posts/proxy-pattern/ Источник изображения: https://www.patterns.dev/posts/proxy-pattern/](https://habrastorage.org/getpro/habr/upload_files/967/2a0/a6f/9672a0a6f64dc5567519891043008ca8.png)
Отлично! Давайте посмотрим, что происходит, когда мы пытаемся изменить или получить свойство.
![Источник изображения: https://www.patterns.dev/posts/proxy-pattern/ С примером кода можно ознакомиться по ссылке: https://codesandbox.io/embed/cocky-bird-rkgyo Источник изображения: https://www.patterns.dev/posts/proxy-pattern/ С примером кода можно ознакомиться по ссылке: https://codesandbox.io/embed/cocky-bird-rkgyo](https://habrastorage.org/getpro/habr/upload_files/975/336/45e/97533645e8a9d4faae3e0cbda9227026.png)
При обращении к свойству name прокси‑сервер возвращает предложение следующего вида: The value of name is John Doe.
При изменении свойства age прокси вернул предыдущее и новое значение этого свойства: Changed age from 42 to 43.
Прокси может быть полезен для добавления валидации. Пользователь не должен иметь возможность изменить возраст человека на строковое значение или дать ему пустое имя. Или, если пользователь пытается получить доступ к несуществующему свойству объекта, мы должны сообщить ему об этом.
![Источник изображения: https://www.patterns.dev/posts/proxy-pattern/ Источник изображения: https://www.patterns.dev/posts/proxy-pattern/](https://habrastorage.org/getpro/habr/upload_files/6aa/b49/111/6aab49111c571de4fa6ecbcd08e68c64.png)
Давайте посмотрим, что происходит, когда мы пытаемся передать ошибочные значения!
С примером кода можно ознакомиться по ссылке:
https://codesandbox.io/embed/focused-rubin-dgk2v
Прокси позаботился о том, чтобы мы не модифицировали объект person с ошибочными значениями, что помогает нам сохранять наши данные в чистоте!
Паттерн Prototype («Прототип»)
Совместное использование свойств многими объектами одного типа.
Паттерн Prototype — это полезный способ совместного использования свойств многими объектами одного типа. Prototype — это нативный объект JavaScript, и к нему могут обращаться объекты через цепочку prototype.
В наших приложениях нам часто приходится создавать множество объектов одного типа. Полезный способ сделать это — создать несколько экземпляров класса ES6.
Допустим, мы хотим создать много собак! В нашем примере собаки умеют не так уж много: у них просто есть имя, и они могут лаять (прим. перев. — метод bark)!
![Источник изображения: https://www.patterns.dev/posts/prototype-pattern/ Источник изображения: https://www.patterns.dev/posts/prototype-pattern/](https://habrastorage.org/getpro/habr/upload_files/4fa/b5f/240/4fab5f24095d4196a1594584c4844749.png)
Обратите внимание, что конструктор содержит свойство name, а сам класс содержит свойство bark. При использовании классов ES6 все свойства, определенные для самого класса, в данном случае bark, автоматически добавляются в prototype.
Мы можем увидеть prototype непосредственно через доступ к свойству prototype конструктора или через свойство __proto__ любого экземпляра.
![Источник изображения: https://www.patterns.dev/posts/prototype-pattern/ Источник изображения: https://www.patterns.dev/posts/prototype-pattern/](https://habrastorage.org/getpro/habr/upload_files/d43/ae6/70c/d43ae670cf2aa3a3058a3637f9d9e5a8.png)
Значение __proto__ в любом экземпляре конструктора — это прямая ссылка на prototype конструктора! Всякий раз, когда мы пытаемся получить доступ к свойству объекта, которое не существует непосредственно в объекте, JavaScript спускается вниз по цепочке prototype, чтобы проверить, доступно ли свойство в цепочке prototype.
![Источник изображения: https://www.patterns.dev/posts/prototype-pattern/ Источник изображения: https://www.patterns.dev/posts/prototype-pattern/](https://habrastorage.org/getpro/habr/upload_files/011/bb1/de1/011bb1de17e82ddd7b86f84a0b44b53f.png)
Модель prototype очень эффективна при работе с объектами, которые должны иметь доступ к одним и тем же свойствам. Вместо того чтобы каждый раз создавать дубликат свойства, мы можем просто добавить свойство в prototype, поскольку все экземпляры имеют доступ к объекту‑prototype.
Поскольку все экземпляры имеют доступ к prototype, легко добавлять свойства к prototype даже после создания экземпляров.
Скажем, наши собаки должны уметь не только лаять, но и играть! Мы можем сделать это возможным, добавив свойство play в prototype.
С примером кода можно ознакомиться по ссылке.
Термин «цепочка прототипов» (прим. перев. — prototype chain) указывает на то, что здесь может быть более одного шага. Действительно! До сих пор мы видели только то, как можно получить доступ к свойствам, которые непосредственно доступны на первом объекте, на который ссылается __proto__. Однако у самих prototype тоже есть объект __proto__.
Давайте создадим другой тип собаки, суперсобаку! Эта собака должна унаследовать все от обычной собаки, но она также должна уметь летать. Мы можем создать суперсобаку, расширив класс Dog и добавив метод fly.
![Источник изображения: https://www.patterns.dev/posts/prototype-pattern/ Источник изображения: https://www.patterns.dev/posts/prototype-pattern/](https://habrastorage.org/getpro/habr/upload_files/e1a/5bd/f64/e1a5bdf648ebb0e92488209a0b501005.png)
Давайте создадим летающую собаку по имени Дейзи, и пусть она лает и летает!
С примером кода можно ознакомиться по ссылке.
У нас есть доступ к методу bark, поскольку мы расширили класс Dog. Значение __proto__ в prototype SuperDog указывает на объект Dog.prototype!
![Источник изображения: https://www.patterns.dev/posts/prototype-pattern/ Источник изображения: https://www.patterns.dev/posts/prototype-pattern/](https://habrastorage.org/getpro/habr/upload_files/556/bf7/4e4/556bf74e45e3bb3f110e9896910a39e2.png)
Становится понятно, почему это называется цепочкой prototype: когда мы пытаемся получить доступ к свойству, которое недоступно непосредственно объекту, JavaScript рекурсивно обходит все объекты, на которые указывает __proto__, пока не найдет это свойство!
Object.create
Метод Object.create позволяет нам создать новый объект, которому мы можем явно передать значение его prototype.
![Источник изображения: https://www.patterns.dev/posts/prototype-pattern/ Источник изображения: https://www.patterns.dev/posts/prototype-pattern/](https://habrastorage.org/getpro/habr/upload_files/183/536/e9d/183536e9dda8de187db53732530490c9.png)
Хотя у самого pet1 нет никаких свойств, у него есть доступ к свойствам цепочки его prototype! Поскольку мы передали объект dog в качестве prototype pet1, мы можем получить доступ к свойству bark.
С примером кода можно ознакомиться по ссылке.
Отлично! Object.create — это простой способ позволить объектам напрямую наследовать свойства от других объектов, указывая prototype вновь создаваемого объекта. Новый объект может получить доступ к новым свойствам, пройдя по цепочке prototype.
Паттерн Prototype позволяет нам легко позволить объектам получать доступ к свойствам других объектов и наследовать их. Поскольку цепочка prototype позволяет нам получить доступ к свойствам, которые не определены непосредственно в самом объекте, мы можем избежать дублирования методов и свойств, тем самым уменьшая объем используемой памяти.
Это ознакомительная часть перевода учебника https://www.patterns.dev/. С переводом всей первой части учебника можно ознакомиться здесь.
P. S. на данный момент выложено в виде pdf, в дальнейшем планируется полноценная публикация на github для удобства изучения