Паттерны проектирования в JavaScript

Что такое паттерн проектирования?

В сфере разработки программного обеспечения паттерн проектирования (design pattern) — это повторяемая архитектурная конструкция, представляющая собой решение проблемы проектирования в рамках некоторого часто возникающего контекста. Паттерны проектирования представляют собой обобщение опыта профессиональных разработчиков ПО. Паттерн проектирования можно рассматривать как некий шаблон, в соответствии с которым пишут программы.

Зачем нужны паттерны проектирования?

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

Самое важное здесь, пожалуй, то, что применение паттернов даёт разработчикам ПО нечто вроде словаря общеизвестных терминов, которые весьма полезны, например, при разборе чужого кода. Паттерны раскрывают предназначение тех или иных фрагментов программы для тех, кто пытается разобраться с устройством какого-нибудь проекта.

Например, если вы используете паттерн «Декоратор», это тут же сообщит новому программисту, пришедшему в проект, о том, какие именно задачи решает некий фрагмент кода и зачем он нужен. Благодаря этому такой программист сможет больше времени уделить практическим задачам, которые решает программа, а не попыткам понять её внутреннее устройство.

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

Паттерн «Модуль»

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

Модули — это составная часть любого современного JavaScript-приложения. Они помогают поддерживать чистоту кода, способствуют разделению кода на осмысленные фрагменты и помогают его организовывать. В JavaScript существует множество способов создания модулей, одним из которых является паттерн «Модуль» (Module).

В отличие от других языков программирования, JavaScript не имеет модификаторов доступа. То есть, переменные нельзя объявлять как приватные или публичные. В результате паттерн «Модуль» используется ещё и для эмуляции концепции инкапсуляции.

Этот паттерн использует IIFE (Immediately-Invoked Functional Expression, немедленно вызываемое функциональное выражение), замыкания и области видимости функций для имитации этой концепции. Например:

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

В результате переменные и функции, объявленные внутри IIFE, скрыты от механизмов, находящихся во внешней по отношению к ним области видимости. Они оказываются приватными сущностями константы myModule.

После того, как этот код будет выполнен, myModule будет выглядеть следующим образом:

То есть, обращаясь к этой константе, можно вызвать общедоступный метод объекта publicMethod(), который, в свою очередь, вызовет приватный метод privateMethod(). Например:

Паттерн «Открытый модуль»

Паттерн «Открытый модуль» (Revealing Module) представляет собой немного улучшенную версию паттерна «Модуль», которую предложил Кристиан Хейльманн. Проблема паттерна «Модуль» заключается в том, что нам приходится создавать публичные функции только для того, чтобы обращаться к приватным функциям и переменным.

В рассматриваемом паттерне мы назначаем свойствам возвращаемого объекта приватные функции, которые хотим сделать общедоступными. Именно поэтому данный паттерн и называют «Открытый модуль». Рассмотрим пример:

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

После выполнения IIFE myRevealingModule выглядит так:

Мы можем, например, вызвать метод myRevealingModule.setName('Mark'), который представляет собой ссылку на внутреннюю функцию publicSetName. Метод myRevealingModule.getName() ссылается на внутреннюю функцию publicGetName. Например:

Рассмотрим преимущества паттерна «Открытый модуль» перед паттерном «Модуль»:

  • «Открытый модуль» позволяет делать общедоступными скрытые сущности модуля (и снова скрывать их, если нужно), модифицируя, для каждой из них, лишь одну строку в объекте, возвращаемом после выполнения IIFE.
  • Возвращаемый объект не содержит определения функций. Всё, что находится справа от имён его свойств, определено в IIFE. Это способствует чистоте кода и упрощает его чтение.

Модули в ES6

До выхода стандарта ES6 в JavaScript не было стандартного средства для работы с модулями, в результате разработчикам приходилось использовать сторонние библиотеки или паттерн «Модуль» для реализации соответствующих механизмов. Но с приходом ES6 в JavaScript появилась стандартная система модулей.

Модули ES6 хранятся в файлах. Один файл может содержать лишь один модуль. Всё, что находится внутри модуля, по умолчанию является приватным. Функции, переменные и классы можно делать публичными с использованием ключевого слова export. Код внутри модуля всегда выполняется в строгом режиме.

Экспорт модуля

Есть два способа экспорта функции или переменной, объявленной в модуле:

  • Экспорт выполняется путём добавления ключевого слова export перед объявлением функции или переменной. Например:

  • Экспорт выполняется путём добавления ключевого слова export в конец кода с перечислением имён функций и переменных, которые нужно экспортировать. Например:

Импорт модуля

Так же, как существуют два способа экспорта, есть и два способа импорта модулей. Делается это с использованием ключевого слова import:

  • Импорт нескольких избранных элементов. Например:

  • Импорт всего, что экспортирует модуль. Например:

Псевдонимы для экспортируемых и импортируемых сущностей

Если имена экспортируемых в код функций или переменных могут вызвать коллизию, их можно изменить при экспорте или при импорте.

Для переименования сущностей при экспорте можно поступить так:

Для переименования сущностей при импорте используется такая конструкция:

Паттерн «Синглтон»

Паттерн «Синглтон» или «Одиночка» (Singleton) представляет собой объект, который может существовать лишь в единственном экземпляре. В рамках применения этого паттерна новый экземпляр некоего класса создаётся в том случае, если он пока не создан. Если же экземпляр класса уже существует, то, при попытке обращения к конструктору, возвращается ссылка на соответствующий объект. Последующие вызовы конструктора всегда будут возвращать тот же самый объект.

Фактически, то, что мы называем паттерном «Синглтон», имелось в JavaScript всегда, но называют это не «Синглтоном», а «объектным литералом». Рассмотрим пример:

Так как каждый объект в JavaScript занимает собственную область памяти и не делит её с другими объектами, всякий раз, когда мы обращаемся к переменной user, мы получаем ссылку на один и тот же объект.

Паттерн «Синглтон» можно реализовать с использованием функции-конструктора. Выглядит это так:

Когда вызывается функция-конструктор, она, в первую очередь, проверяет, существует ли объект instance. Если соответствующая переменная не инициализирована, в instance записывают this. Если же в переменной уже есть ссылка на объект, конструктор просто возвращает instance, то есть — ссылку на уже существующий объект.

Паттерн «Синглтон» можно реализовать с использованием паттерна «Модуль». Например:

Здесь мы создаём новый экземпляр user, вызывая метод singleton.getInstance(). Если экземпляр объекта уже существует, то этот метод просто возвратит его. Если же такого объекта пока нет, метод создаёт его новый экземпляр, вызывая функцию-конструктор User.

Паттерн «Фабрика»

Паттерн «Фабрика» (Factory) использует для создания объектов так называемые «фабричные методы». При этом не требуется указывать классы или функции-конструкторы, которые применяются для создания объектов.

Этот паттерн используется для создания объектов в случаях, когда не нужно делать общедоступной логику их создания. Паттерн «Фабрика» может быть использован в том случае, если нужно создавать различные объекты в зависимости от специфических условий. Например:

Здесь созданы классы Car и Truck, которые предусматривают использование неких стандартных значений. Они применяются для создания объектов car и truck. Также здесь объявлен класс VehicleFactory, который используется для создания новых объектов на основе анализа свойства vehicleType, передаваемого соответствующему методу возвращаемого им объекта в объекте с параметрами options. Вот как со всем этим работать:

Здесь создан объект factory класса VehicleFactory. После этого можно создавать объекты классов Car или Truck, вызывая метод factory.createVehicle() и передавая ему объект options со свойством vehicleType, установленным в значение car или truck.

Паттерн «Декоратор»

Паттерн «Декоратор» (Decorator) используется для расширения функционала объектов без модификации существующих классов или функций-конструкторов. Этот паттерн можно использовать для добавления к объектам неких возможностей без модификации кода, который ответственен за их создание.

Вот простой пример использования этого паттерна:

Рассмотрим теперь практический пример применения этого паттерна. Предположим, стоимость автомобилей зависит от их особенностей, от имеющихся у них дополнительных функций. Без использования паттерна «Декоратор» нам, для описания этих автомобилей, пришлось бы создавать разные классы для разных комбинаций этих дополнительных функций, в каждом из которых присутствовал бы метод для нахождения стоимости автомобиля. Например, это может выглядеть так:

Благодаря рассматриваемому паттерну можно создать базовый класс Car, описывающий, скажем, автомобиль в базовой комплектации, стоимость которого выражается некоей фиксированной суммой. После этого стандартный объект, создаваемый на основе этого класса, можно расширять с использованием функций-декораторов. Стандартный «автомобиль», обработанный такой функцией, получает новые возможности, что, кроме того, влияет на его цену. Например, эту схему можно реализовать так:

Здесь мы сначала создаём базовый класс Car, используемый для создания объектов, представляющих автомобили в стандартной комплектации. Затем создаём несколько функций-декораторов, которые позволяют расширять объекты базового класса Car дополнительными свойствами. Эти функции принимают соответствующие объекты в качестве параметров. После этого мы добавляем в объект новое свойство, указывающее на то, какой новой возможностью будет оснащён автомобиль, и переопределяем функцию cost объекта, которая теперь возвращает новую стоимость автомобиля. В результате, для того, чтобы «оснастить» автомобиль стандартной конфигурации чем-то новым, мы можем воспользоваться следующей конструкцией:

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

Оригинальный материал статьи: Паттерны проектирования в JavaScript

Оставить комментарий

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