Паттерны проектирования необходимо использовать для создания кода, чтобы он легко поддавался изменениям и дополнениям, применялся многократно и становился достаточно организован. “Gang of Four” включает в себя 23 уникальных паттерна проектирования, которые можно использовать на разных платформах. Давайте узнаем, как Gang of Four может быть реализована в JavaScript.
Давайте рассмотрим паттерны проектирования из ООП, описанные в "Gang Of Four", и изучим, как они имплементированы в JavaScript.
Что такое паттерны проектирования?
Прежде всего, необходимо понять истинное значение понятия «паттерны проектирования». Как разработчик программного обеспечения вы можете писать код «любым способом». Однако именно лучшие практики оказывают решающее влияние на то, как вам удается поддерживать код. Код, написанный с особой тщательностью, прослужит дольше, чем выполненный дилетантами. Это означает, что вам не нужно беспокоиться о масштабируемости или обслуживании, если вы сразу выбрали правильный стиль кодирования.
Роль паттернов проектирования заключается в том, что они позволяют создавать решения, не усложняющие общую задачу.
Паттерны помогут вам создать интерактивные объекты и конструкции, которые будут использоваться многократно.
Паттерны проектирования являются неотъемлемой концепцией объектно-ориентированного программирования (ООП).
Gang Of Four — это ваш ключ к шаблонам проектирования. В настоящее время в Gang Of Four существует 23 паттерна. Эти паттерны классифицированы по трем уникальным группам: Порождающие, Структурные и Поведенческие.
Порождающие паттерны проектирования (Creational Design Pattern) в JavaScript
Абстрактная фабрика (Abstract Factory);
Строитель (Builder);
Фабрика (Factory);
Прототип (Prototype);
Синглтон (Singleton).
Абстрактная фабрика
Что такое фабрика? Если вы спросите ребенка, как бы он ее описал? Фабрика — это не что иное, как место, где мы производим вещи. Например, на фабрике игрушек вы увидите их производство. Подобно этому определению в реальном времени, фабрика в JavaScript — это место, где Object создает другие объекты. Не все фабрики игрушек производят плюшевых мишек и трансформеров, верно? Существуют отдельные фабрики, для разных типов игрушек. Фабрика игрушек всегда работает с определенной тематикой. Аналогично поступает и Абстрактная фабрика (Abstract factory). Объекты, которые выходят из Abstract factory, будут иметь общую тему.
JavaScript не поддерживает наследование на основе классов. Поэтому имплементация паттерна Abstract factory весьма интересна.
Попробуем разобраться с Abstract factory в JavaScript на примере.
Мне нужно создать программное обеспечение для фабрики игрушек.
У меня есть отдел, который производит игрушки по мотивам фильма «Трансформеры». И еще один отдел для игрушек на тему «Звездных войн».
Оба отдела имеют несколько общих характеристик и свою уникальную тему.
function StarWarsFactory(){
this.create = function(name){
return new StarWarsToy(name)
}}
function TransformersFactory(){
this.create = function(name){
return new TransformerToy(name)
}}
function StarWarsToy(name){
this.nameOfToy = name;
this.displayName = function(){
console.log("My Name is "+this.nameOfToy);
}}
function TransformersToy(name){
this.nameOfToy = name;
this.displayName = function(){
console.log("My Name is "+this.nameOfToy);
}}
function buildToys(){
var toys = [];
var factory_star_wars = new StarWarsFactory();
var factory_transformers = new TransformersFactory();
toys.push(factory_star_wars.create("Darth Vader"));
toys.push(factory_transformers.create("Megatron"));
for(let toy of toys)
console.log(toy.displayName());
}
Строитель
Роль строителя (builder) заключается в создании сложных объектов. Клиент получает конечный объект, не беспокоясь о реально проделанной работе. Чаще всего паттерн builder представляет собой инкапсуляцию составного объекта. Главным образом потому, что весь процесс является сложным и повторяющимся.
Давайте посмотрим, как реализовать нашу фабрику игрушек с помощью паттерна builder.
У меня есть фабрика игрушек.
В фабрике есть отдел по созданию игрушек из "Звездных войн".
В этом отделе я хочу сделать много фигурок Дарта Вейдера.
Создание Дарта Вейдера — это двухэтапный процесс. Мне нужно изготовить игрушку, а затем указать ее окраску.
function StarWarsFactory(){
this.build = function(builder){
builder.step1();
builder.step2();
return builder.getToy();
}}
function DarthVader_Builder(){
this.darth = null;
this.step1 = function () {
this.darth = new DarthVader();
}
this.step2 = function () {
this.darth.addColor();
}
this.getToy = function () {
return this.darth;
}
}
function DarthVader () {
this.color = '';
this.addColor = function(){
this.color = 'black';
}
this.say = function () {
console.log("I am Darth, and my color is "+this.color);
}
}
function build(){
let star_wars = new StarWarsFactory();
let darthVader = new DarthVader_Builder();
let darthVader_Toy = star_wars.build(darthVader);
darthVader_Toy.say();
}
Фабрика
Роль фабрики заключается в производстве схожих объектов, обладающих одинаковыми характеристиками. Это помогает легко управлять, обслуживать и манипулировать объектами. Например, на нашей фабрике каждая игрушка будет обладать определенной информационной составляющей. На ней будет указана дата приобретения, происхождение и категория.
var ToyFactory = function(){
this.createToy = function(type)
{
var toy;
if(type == "starwars")
{
toy = new StarWars();
}
toy.origin = "Origin";
toy.dop = "2/22/2022";
toy.category="fantasy";
}
}
Прототип
Часто возникает необходимость создать новый объект, имеющий значения по умолчанию от другого родительского объекта. Это предотвращает создание объектов с неинициализированными значениями. Для создания таких объектов можно использовать паттерн Прототип (Prototype).
Паттерны Prototype также известны как паттерн свойств (Properties Pattern).
На нашей фабрике игрушек есть отдел "Звездные войны", который производит множество персонажей (characters).
У каждого Character есть поле жанра, срока годности и статуса. Эти поля будут одинаковыми для всех игрушек из отдела "Звездные войны".
function Star_Wars_Prototype(parent){
this.parent = parent;
this.duplicate = function ()
{
let starWars = new StarWarsToy();
starWars.genre = parent.genre;
starWars.expiry = parent.expiry;
starWars.status = parent.status;
return starWars;
};
}
function StarWarsToy(genre, expiry, status){
this.genre = genre;
this.expiry = expiry;
this.status = status;
}
function build () {
var star_wars_toy = new StarWarsToy('fantasy', 'NA', 'Jan');
var new_star_wars_toy = new Star_Wars_Prototype(star_wars_toy);
//When you are ready to create
var darth = new_star_wars_toy.duplicate();
}
Синглтон
Паттерн Синглтон (Singleton) соответствует "единственному экземпляру". У данного объекта может быть только один экземпляр. Когда в системе есть данные, которые нужно координировать из одного места, можно использовать этот паттерн.
var instance;
function createInstance() {
var object = new Object("I am the instance");
return object;
}
return {
getInstance: function () {
if (!instance) {
instance = createInstance();
}
return instance;
}
};
})();
function run() {
var instance1 = Singleton.getInstance();
var instance2 = Singleton.getInstance();
console.log("Same instance? " + (instance1 === instance2));
}
Структурные шаблоны проектирования
Адаптер (Adapter);
Мост (Bridge);
Компоновщик (Composite);
Декоратор (Decorator);
Фасад (Facade);
Приспособленец (Flyweight);
Прокси (Proxy).
Адаптер
Шаблон проектирования Адаптер (Adapter) используется, когда свойства или методы должны быть переведены из одного объекта в другой. Этот паттерн чрезвычайно полезен, когда компоненты с несовпадающими интерфейсами должны работать друг с другом. Шаблон Adapter также известен как шаблон Wrapper (Обертка).
Давайте разберем это на примере нашей Фабрики игрушек:
У фабрики игрушек есть отдел доставки.
Мы планируем осуществить миграцию от старого отдела доставки к новому.
Однако нам необходимо сохранить старые методы доставки для текущих запасов.
// old interface
function Shipping() {
this.request = function (zipStart, zipEnd, weight) {
// ...
return "$49.75";
}
}
// new interface
function AdvancedShipping() {
this.login = function (credentials) { /* ... */ };
this.setStart = function (start) { /* ... */ };
this.setDestination = function (destination) { /* ... */ };
this.calculate = function (weight) { return "$39.50"; };
}
// adapter interface
function ShippingAdapter(credentials) {
var shipping = new AdvancedShipping();
shipping.login(credentials);
return {
request: function (zipStart, zipEnd, weight) {
shipping.setStart(zipStart);
shipping.setDestination(zipEnd);
return shipping.calculate(weight);
}
};
}
function run() {
var shipping = new Shipping();
var credentials = { token: "StarWars-001" };
var adapter = new ShippingAdapter(credentials);
// original shipping object and interface
var cost = shipping.request("78701", "10010", "2 lbs");
console.log("Old cost: " + cost);
// new shipping object with adapted interface
cost = adapter.request("78701", "10010", "2 lbs");
console.log("New cost: " + cost);
}
Мост
Мост (Bridge) — это известный архитектурный паттерн высокого уровня. С его помощью предлагаются различные уровни абстракции. Как результат, Objects будут слабо связаны друг с другом. Каждый Object, который становится компонентом, будет иметь свой собственный интерфейс.
В ассортименте нашей фабрики игрушек, где мы производим игрушки "Звездные войны", есть две разновидности. Одним набором игрушек можно управлять с помощью пульта дистанционного управления. Другой работает от батареек и издает различные звуки. Паттерн Bridge помогает в построении этой высокоуровневой архитектуры.
this.output = output;
this.left = function () { this.output.left(); }
this.right = function () { this.output.right(); }
};
var Battery_Operation= function (output) {
this.output = output;
this.move = function () { this.output.move(); }
this.wheel = function () { this.output.zoom(); }
};
var Remote_Controlled_Toy = function () {
this.left = function () { console.log("Move Left"); }
this.right = function () { console.log("Move Right"); }
};
var Battery_Operated_Toy = function () {
this.move = function () { console.log("Sound waves"); }
this.wheel = function () { console.log("Sound volume up"); }
};
function run() {
var remote_control = new Remote_Control();
var battery_operation = new Battery_Operation();
var star_wars_type_1 = new Remote_Controlled_Toy(remote_control);
var star_wars_type_2 = new Battery_Operated_Toy(battery_operation);
star_wars_type_1.left();
star_wars_type_2.wheel();
}
Компоновщик
Как следует из названия, паттерн Компоновщик (Composite) создает объекты, которые являются примитивами или коллекцией объектов. Это помогает при построении глубоко вложенных структур.
На нашей фабрике игрушек паттерн composite помогает следующим образом:
У нас есть два отдела, один из которых — мануальный, а второй — автоматизированный.
В мануальном отделе у нас есть группа игрушек (листовые объекты. листья),
В автоматизированном отделе у нас есть еще один набор игрушек (листья).
this.children = [];
this.name = name;
}
Node.prototype = {
add: function (child) {
this.children.push(child);
}
}
function run() {
var tree = new Node("Star_Wars_Toys");
var manual = new Node("Manual")
var automate = new Node("Automated");
var darth_vader = new Node("Darth Vader");
var luke_skywalker = new Node("Luke Skywalker");
var yoda = new Node("Yoda");
var chewbacca = new Node("Chewbacca");
tree.add(manual);
tree.add(automate);
manual.add(darth_vader);
manual.add(luke_skywalker);
automate.add(yoda);
automate.add(chewbacca);
}
Декоратор
Паттерн Декоратор (Decorator) расширяет свойства и методы объекта, добавляя ему новое поведение в течение рантайма. Несколько декораторов можно использовать для добавления или переопределения фактических функциональных возможностей объекта.
В нашей фабрике игрушек у нас есть функция для присвоения игрушкам наименования. И у нас есть дополнительный декоратор для указания жанра.
this.name = name;
this.display = function () {
console.log("Toy: " + this.name);
};
}
var DecoratedToy = function (genre, branding) {
this.toy = toy;
this.name = user.name; // ensures interface stays the same
this.genre = genre;
this.branding = branding;
this.display = function () {
console.log("Decorated User: " + this.name + ", " +
this.genre + ", " + this.branding);
};
}
function run() {
var toy = new User("Toy");
var decorated = new DecoratedToy(toy, "fantasy", "Star Wars");
decorated.display();
}
Фасад
Шаблон проектирования Facade (Фасад) предлагает высокоуровневый интерфейс свойств и методов. Эти свойства и методы могут использоваться подсистемами.
Приспособленец
Этот паттерн используется, когда множество мелких объекты необходимо разделить между более крупными и составными Objects. Objects будут иммутабельными. Почему? Поскольку мелкие объекты используются совместно с другими, они не должны быть изменены. Шаблон Приспособленец (Легковесный) (Flyweight) представлен в движке Javascript (Javascript Engine). Например, Javascript Engine поддерживает список строк, которые иммутабельны и могут использоваться совместно в разных приложениях.
В нашей фабрике игрушек:
У каждой игрушки есть жанр.
У каждой игрушки есть страна-производитель.
У каждой игрушки есть год производства. Эти свойства можно хранить в модели Flyweight.
this.genre = genre;
this.country = country;
this.year = year;
};
var FlyWeightFactory = (function () {
var flyweights = {};
return {
get: function (genre, country, year) {
if (!flyweights[genre + country]) {
flyweights[genre + country] =
new Flyweight(genre, country, year);
}
return flyweights[genre + country];
}
}
})();
function ToyCollection() {
var toys = {};
return {
add: function (genre, country, year, brandTag) {
toys[brandTag] =
new Toy(genre, country, year, brandTag);
}
};
}
var Toy = function (genre, country, year, brandTag) {
this.flyweight = FlyWeightFactory.get(genre, country, year, brandTag);
this.brandTag = brandTag;
}
function build() {
var toys = new ToyCollection();
toys.add("Fantasy", "USA", "2021", "StarWars_01");
toys.add("Fantasy", "USA", "2021", "Transformers_01");
}
Заместитель
Паттерн Заместитель (Proxy) предлагает объект-плейсхолер вместо фактического. Объект-плейсхолдер контролировать доступ к значению фактического объекта.
Например, наша фабрика игрушек расположена во многих уголках мира. В каждом месте производится определенное количество игрушек. При помощи паттерна Proxy проще понять, сколько игрушек производится в каждом месте.
this.getLatLng = function (address) {
if (address === "Hong Kong") {
return "52.3700° N, 4.8900° E";
} else if (address === "North America") {
return "51.5171° N, 0.1062° W";
} …. };
}
function GeoProxy() {
var coder = new GeoCoder();
var geoCollector = {};
return {
getLatLng: function (location) {
if (!geoCollector[location]) {
geoCollector[location] = coder.getLatLng(location);
}
return geoCollector[location];
}};
};
function run() {
var geo = new GeoProxy();
geo.getLatLng("Hong Kong");
geo.getLatLng("North America");
}
Поведенческие паттерны проектирования
Цепочка обязанностей (Chain of Responsibility);
Команда (Command);
Интерпретатор (Interpreter);
Итератор (Iterator);
Посредник (Mediator);
Хранитель (Memento);
Наблюдатель (Observer);
Состояние (State);
Стратегия (Strategy);
Шаблон (Template);
Посетитель (Visitor).
Цепочка обязанностей
Любой, кто использует Javascript, хотя бы раз сталкивался с концепцией всплытия событий (event-bubbling). Это когда события передаются через вложенные элементы управления. Один из вложенных элементов управления может быть выбран для обработки всплывающего события. Для обработки такого поведения можно использовать паттерн Цепочка обязанностей (Chain of Responsibility). Если быть точным, этот паттерн широко используется в JQuery.
Команда
Часто Objects имеют общий набор событий, которые необходимо обработать. При построении Objects, в которых могут быть инкапсулированы действия по обработке этих событий помогают объекты Команда (Command). Чаще всего паттерн Command используется для централизации функциональных возможностей. Например, операция отмены в любом приложении является наглядным примером паттерна Command. Независимо от того, вызываете ли вы ее из выпадающего меню или с клавиатуры, активизируется одна и та же функциональность.
Интерпретатор
Не все решения одинаковы. Во многих приложениях может понадобится добавить дополнительные строки кода, чтобы манипулировать вводом или выводом, который будет демонстрироваться пользователю. В этом случае вывод зависит от приложения.
В нашем примере с фабрикой все игрушки из "Звездных войн" должны иметь префикс с тэглайном: "Да пребудет с вами сила". А все игрушки "Боб-Строитель" должны иметь префикс "Вы готовы!". Эту дополнительную кастомизацию можно осуществить с помощью шаблона Интерпретатор (Interpreter).
this.brandTag = brandTag;
}
Prefix.prototype = {
interpret: function () {
if (this.brandTag == “Star Wars”) {
return “May the Force be with You”;
}
else if (this.brandTag == “Bob the Builder”) {
return “Are you Ready?”;
}
}
}
function run() {
var toys = [];
toys.push(new Prefix(“Star Wars”));
toys.push(new Prefix(“Bob the Builder”));
for (var i = 0, len = toys.length; i < len; i++) {
console.log(toys[i].interpret());
}
}
Итератор
Как следует из названия, паттерн Итератор (Iterator) можно применить для того, чтобы определить, как объект или коллекция объектов должны быть эффективно зациклены. В Javascript встречаются несколько распространенных форм циклов: while, for, for-of, for-in и do while. Используя паттерн iterator, вы можете разработать свой собственный способ циклической обработки объектов, наиболее гибкий для создаваемого приложения.
Посредник
Еще один шаблон проектирования, который работает, как следует из его названия, — это Посредник (Mediator). Паттерн обеспечивает центральный пункт управления для группы объектов. Он активно используется для управления состоянием. Когда один из объектов изменяет состояние своих свойств, это может быть легко передано другим объектам.
Вот простой пример того, как помогает паттерн проектирования Mediator:
this.name = name;
this.chatroom = null;
}
//define a Itprototype for participants with receive and send implementation
var Talkie = function () {
var participants = {};
return {
register: function (participant) {
participants[participant.name] = participant;
participant.talkie = this;
},
send: function (message, from, to) {
if (to) { // single message
to.receive(message, from);
} else { // broadcast message
for (key in participants) {
if (participants[key] !== from) {
participants[key].receive(message, from);
}
}
}
}
};
};
function letsTalk() {
var A = new Participant("A");
var B = new Participant("B");
var C = new Participant("C");
var D = new Participant("D");
var talkie = new Talkie();
talkie.register(A);
talkie.register(B);
talkie.register(C);
talkie.register(D);
A.send("I love you B.");
B.send("No need to broadcast", A);
C.send("Ha, I heard that!");
D.send("C, do you have something to say?", C);
}
Хранитель
Хранитель (Memento) представляет собой репозиторий, в котором хранится состояние объекта. В приложении могут быть сценарии, когда состояние объекта необходимо сохранить и восстановить. В большинстве случаев свойство сериализации и де-сериализации объекта с помощью JSON помогает в имплементации этого паттерна проектирования.
this.name = name;
this.country = country;
this.city = city;
this.year = year;
}
Toy.prototype = {
encryptLabel: function () {
var memento = JSON.stringify(this);
return memento;
},
decryptLabel: function (memento) {
var m = JSON.parse(memento);
this.name = m.name;
this.country = m.country;
this.city = m.city;
this.year = m.year;
console.log(m);
}
}
function print() {
var darth = new Toy("Darth Vader", "USA", "2022");
darth.encryptLabel();
darth.decryptLabel();
}
Наблюдатель
Паттерн Наблюдатель (Observer) — один из наиболее широко используемых. Он имеет уникальную имплементацию для различных платформ, таких как Angular и React. На самом деле, это тема, которая требует отдельного обсуждения. При этом, данный паттерн предусматривает модель подписки. Объекты могут подписываться на определенные события. И когда событие произойдет, они получат уведомление. JavaScript — это событийно-ориентированный язык программирования. Большая часть его концепций зависит от паттерна Observer.
Состояние
Итак, давайте начнем с примера. Светофор имеет три цвета: красный, желтый и зеленый. Действие, которое нужно выполнить, отличается для каждого из цветов. Это означает, что существует логика, зависящая от цвета. Любой объект в пределах соответствующего цвета должен будет придерживаться его правил. Аналогично, паттерны state поставляются с определенным набором логики для каждого состояния. Объекты в пределах состояния должны следовать этой логике.
Стратегия
Паттерн проектирования Стратегия (Strategy) используется для инкапсуляции алгоритмов, которые могут быть использованы для выполнения конкретной задачи. На основе определенных предварительных условий изменяется фактическая стратегия (alias-метод (метод псевдонима)), которая должна быть выполнена. Это означает, что алгоритмы, используемые в рамках стратегии, в значительной степени взаимозаменяемы.
На нашей фабрике игрушек мы отправляем продукцию с помощью трех различных компаний: FedEx, USPS и UPS. Окончательная стоимость доставки зависит от компании. Таким образом, мы можем использовать шаблон проектирования Strategy чтобы определить конечную стоимость.
setDistributor: function (company) {
this.company = company;
},
computeFinalCost: function () {
return this.company.compute();
}
};
var UPS = function () {
this.compute = function () {
return "$45.95";
}
};
var USPS = function () {
this.compute = function () {
return "$39.40";
}
};
var Fedex = function () {
this.compute = function () {
return "$43.20";
}
};
function run() {
var ups = new UPS();
var usps = new USPS();
var fedex = new Fedex();
var distributor = new Distributor();
distributor.setDistributor(ups);
console.log("UPS Strategy: " + distributor.computeFinalCost());
distributor.setDistributor(usps);
console.log("USPS Strategy: " + distributor.computeFinalCost());
distributor.setDistributor(fedex);
console.log("Fedex Strategy: " + distributor.computeFinalCost());
}
Шаблон
Паттерн Шаблон (Template) представляет собой последовательность определенных шагов. Когда объект создается в соответствии с этим паттерном, все шаги в шаблоне должны быть выполнены. Конечно, некоторые этапы могут быть кастомизированы в соответствии с создаваемым объектом. Этот паттерн проектирования широко используется в распространенных библиотеках и фреймворках.
Посетитель
И в заключение, у нас есть паттерн проектирования Посетитель (Visitor). Он полезен, когда необходимо определить новую операцию для группы объектов. При этом исходная структура Objects должна оставаться нетронутой. Этот паттерн полезен, когда необходимо расширить фреймворк или библиотеку. В отличие от других паттернов, рассмотренных в этом посте, Visitor не очень широко используется в Javascript. Почему? Движок JavaScript поставляется с более продвинутыми и гибкими способами динамического добавления или удаления свойств из объектов.
var self = this;
this.accept = function (visitor) {
visitor.visit(self);
};
this.getSalary = function () {
return salary;
};
this.setSalary = function (salary) {
salary = salary;
};
};
var ExtraSalary = function () {
this.visit = function (emp) {
emp.setSalary(emp.getSalary() * 2);
};
};
function run() {
var john = new Employee("John", 10000, 10),
var visitorSalary = new ExtraSalary();
john.accept(visitorSalary);
}
}
Заключение
Некоторые паттерны проектирования часто используются движком Javascript. И многие из нас даже не знают, задействуются они или нет. Использование паттернов улучшит производительность и обслуживаемость вашего кода. Именно поэтому в коде следует применять все больше и больше паттернов. Внедрение паттернов в код не обязательно должно происходить в одночасье. Вместо этого осваивайте паттерны и постепенно включайте их в свое приложение.
Перевод статьи подготовлен в рамках специализации "Fullstack Developer".
bromzh
Какой-то позорный код. То var (в 2022, ага), то let. То camelCase, то snake_case. Функции вместо классов. «Методы» вместо прототипов пихаются сразу в this.
«Otus — Цифровые навыки от ведущих экспертов» ©. Такие у вас эксперты?