Возможно, вы уже знакомы с такими фреймворками/библиотеками как ReactJS, Angular или jQuery, но знаете ли вы, зачем они были созданы? Какую проблему решают? Какой шаблон проектирования используют? Шаблоны проектирования очень важно знать, и, могу предположить, некоторые из разработчиков-самоучек могли их пропустить. Так что же такое шаблоны проектирования? Что они делают? Как мы можем их использовать? Почему их следует использовать?


Что такое шаблоны проектирования и зачем их использовать?

Шаблоны проектирования - проверенный способ для решения проблем. Они не включают в себя такие очевидные вещи, как использование for loop для перебора элементов массива. Их используют для решения более сложных проблем, с которыми мы сталкиваемся при разработке больших приложений.

Некоторые из плюсов использования шаблонов проектирования:

  • Не нужно изобретать велосипед (ленивый программист => хороший программист)
  • Находишь общий язык с разработчиками
  • Выглядишь круто и профессионально
  • Если ты самоучка, то это поможет тебе изрядно выделиться среди конкурентов
  • Эти шаблоны проходят сквозь все языки программирования

Типы шаблонов и примеры некоторых из них

- Порождающие шаблоны (Creational): создание новых объектов.

  • Конструктор (Constructor)
  • Модуль (Module)
  • Фабрика (Factory)
  • Синглтон (Singletion)

Структурные шаблоны(Structural): упорядочивают объекты.

  • Декоратор (Decorator)
  • Фасад (Facade)

Поведенческие (Behavioral): как объекты соотносятся друг с другом.

  • Наблюдатель (Observer)
  • Посредник (Mediator)
  • Команда (Command)

Порождающее шаблоны

Эти шаблоны используются для создания новых объектов.

Constructor

Создает новые объекты в их собственной области видимости.

// Constructor Pattern
// Используйте для создания новых объектов в их собственной области видимости.

var Person = function(name, age, favFood) {
	this.name = name;
	this.age = age;
	this.favFood = favFood;
};

// Прототип позволяет всем экземплярам Person ссылаться на него без повторения функции.
Person.prototype.greet = function() {
	console.log(`Hello, my name is ${this.name}, I'm ${this.age} years old, and my favorite food is ${this.favFood}`);
}

// new создает объект {} и передает "this" в конструктор
// Конструктор устанавливает значение для этого объекта и возвращает его.
var bob = new Person('Bob', 22, 'Chicken');
bob.greet(); 
// Hello, my name is Bob, I'm 22 years old, and my favorite food is Chicken

// ES6 / ES2015 Классы
class Vehicle {
	constructor(type, color) {
		this.type = type;
		this.color = color;
	}
	
	getSpecs() {
		console.log(`Type: ${this.type}, Color: ${this.color}`);
	}
};

var someTruck = new Vehicle('Truck', 'red');
someTruck.getSpecs();

Module

Используйте для инкапсуляции методов.

// Module Pattern
// Используется для инкапсуляции кода

var myModule = (function() {
	// Приватная переменная
	var memes = ['cats', 'doge', 'harambe'];
    
	var getMemes = function() {
		return memes;
	};
	
	// возвращает то, к чему вы хотите разрешить доступ в объекте
	// то, как он это возвращает действительно делает его показателем модульного шаблона проектирования
	return {
		getMemes: getMemes
	};
})();

console.log(myModule.getMemes()); // массив мемов
console.log(myModule.memes); // undefined

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

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

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

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

const myModule = (function() {
	const privateVariable = "Hello World";
	
	function privateMethod() {
		console.log(privateVariable);
	}
	
	return {
		publicMethod: function() {
			privateMethod();
		}
	}
})();

myModule.publicMethod(); // Hello World

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

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

const myModule = {
    publicMethod: function() {
        privateMethod();
    }
};

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

// Выводит 'Hello World'
myModule.publicMethod();

Factory

Используйте для того, чтоб упростить создание объектов, проще генерировать экземпляры объектов, не требует использования конструктора.

// Factory Pattern
// Несколько конструкторов для нашей фабрики

function Cat(options) {
	this.sound = 'Meow';
	this.name = options.name;
}

function Dog(options) {
	this.sound = 'Rawr';
	this.name = options.name;
}

// Animal Factory
function AnimalFactory() {}

// Тип Cat по умолчанию
AnimalFactory.prototype.animalType = Cat;

// метод для создания новых животных
AnimalFactory.prototype.createAnimal = function(options) {
	switch(options.animalType) {
		case "cat":
			this.animalType = Cat;
			break;
		case "dog":
			this.animalType = Dog;
			break;
		default:
			this.animalType = Cat;
			break;
	}
	
	return new this.animalType(options);
}

var animalFactory = new AnimalFactory();
var doge = animalFactory.createAnimal({
	animalType: 'dog',
	name: 'Doge'
});

var snowball = animalFactory.createAnimal({name: 'Snowball'});

console.log(doge instanceof Dog);     // true
console.log(doge);                    // выводит doge как cat объект
console.log(snowball instanceof Cat); // true
console.log(snowball);                // выводит snowball как cat объект

Фабричная функция - это функция, которая принимает несколько аргументов и возвращает новый объект, состоящий из этих аргументов. В JavaScript любая функция может возвращать объект. Если она делает это без ключевого слова new, то её можно назвать фабричной. Фабрика - отделяет клиента от конкретных классов, экземпляры которых он хочет получить.

Фабрика позволяет создавать объекты не напрямую (через ключевое слово new), а через вызов методы у какого-то другого объекта. То есть вызов через new происходит, только это делается "под капотом". Фабрика используется для того, чтобы избавить пользователя от зависимостей от класса/классов. Плюс фабрика используется в том случае, когда клиент не знает какой класс будет использоваться.

То есть мы выносим логику по созданию объектов в конкретный модуль.

class Car {
    constructor(doors = 4, state = "brand new", color = "silver") {
        this.doors = doors;
        this.state = state;
        this.color = color;
    }
}

class Truck {
    constructor(state = "used", wheelSize = "large", color = "blue") {
        this.state = state;
        this.wheelSize = wheelSize;
        this.color = color;
    }
}

class VehicleFactory {
    vehicleClass = Car;
	
    createVehicle = (type, props) => {
        switch(type) {
            case "car":
                return new this.vehicleClass(props.doors, props.state, props.color);
            case "truck":
                return new this.vehicleClass(props.state, props.wheelSize, props.color);
        }
    }
}

// Let's build a vehicle factory!

const factory = new VehicleFactory();
const car = factory.createVehicle("car", {
    doors: 6,
    color: "green"
});

console.log(JSON.stringify(car)); // {"doors":6,"state":"brand new","color":"green"}

const truck = factory.createVehicle("truck", {
    state: "like new",
    color: "red",
    wheelSize: "small"
});

console.log(JSON.stringify(truck)); // {"doors":"like new","state":"small","color":"red"}

// Let's build a truck factory!

class TruckFactory extends VehicleFactory {
    vehicleClass = Truck;
}

const truckFactory = new TruckFactory();
const bigTruck = truckFactory.createVehicle("truck", {
    state: "omg ... so bad",
    color: "pink",
    wheelSize: "so BIG"
});

console.log(JSON.stringify(bigTruck)); // {"state":"omg ... so bad","wheelSize":"so BIG","color":"pink"}

Singleton

Используйте для того, чтобы ограничиться одним экземпляром объекта.

Этот паттерн произрастает из математической концепции синглтона - одноэлементного множества, то есть множества, содержащего лишь один элемент. Например, множество {null} - это синглтон.

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

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

В качестве основного примера использования паттерна синглтон в крупных популярных фреймворках можно назвать сервисы Angular. В документации по Angular есть отдельная страница, посвящённая разъяснению того, как сделать сервис синглтоном.

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

Рассмотрим пример. Предположим, у нас имеется простейшее приложение, которое используется для подсчёта количества нажатий на кнопки.

Количество щелчков по кнопкам нужно хранить в одном объекте, который реализует следующие возможности:

  • Он позволяет подсчитывать щелчки по кнопкам.
  • Он даёт возможность считывать текущее значение счётчика щелчков.

Если подобный объект не был бы синглтоном (и с каждой кнопкой был бы связан собственный экземпляр такого объекта), то программа неправильно подсчитывала бы число нажатий на кнопки. Кроме того, при таком подходе нужно решить следующую задачу: "Из какого именно объекта, отвечающего за подсчёт щелчков, будут браться данные, выводимые на экран?".

class Singleton {
	constructor() {
		// the class constructor
		if(!Singleton.instance) {
			Singleton.instance = this;
		}
		
		return Singleton.instance;
	}

	publicMethod() {
		console.log('Public Method');
	}
}

const instance = new Singleton();

// prevents new properties from being added to the object
Object.freeze(instance);

export default instance;

Структурные шаблоны

Как создаются объекты и какие взаимоотношения между ними. Расширяет или упрощает функциональность.

Decorator

Используйте, чтоб добавлять новую функциональность объектам (Расширяет функциональность).

// Decorator Pattern
// Простой конструктор

var Person = function (name) {
	this.name = name;
}

Person.prototype.greet = function () {
	console.log(`Hello, my name is ${this.name}`);
}

var uniqueBob = new Person('Bob');

// может быть добавлено к нему без изменения конструктора Person
uniqueBob.hobbies = ['Cooking', 'Running'];

uniqueBob.greet = function() {
	Person.prototype.greet.call(this);
	console.log('My hobbies are: ', this.hobbies);
};

uniqueBob.greet();

// Другой способ
var CoolPerson = function(name, catchPhrase) {
	Person.call(this, name);
	this.catchPhrase = catchPhrase;
};

// включает в себя prototypes от Person
CoolPerson.prototype = Object.create(Person.prototype);

// изменяет прототип
CoolPerson.prototype.greet = function() {
	Person.prototype.greet.call(this);
	console.log(this.catchPhrase);
};

var coolDude = new CoolPerson('Jeff', 'Aaaayyy');
console.log(coolDude);
coolDude.greet();

Facade

Используйте для создания простого интерфейса (упрощает функциональность, как например jQuery).

// Facade Pattern
// абстрагирует от некоторых сложных/неряшлевых вещей

var $ = function (target) {
	return new MemeQuery(target);
};

function MemeQuery (target) {
	this.target = document.querySelector(target);
}

MemeQuery.prototype.html = function(html) {
	this.target.innerHTML = html;
	return this;
};

// теперь, все, что мы будем видеть и использовать это - $
$('#myParagraph').html('Meeemee').html('Some JS design patterns');

// окей, возможно это и не лучший пример...
// просто посмотрите в исходный код jQuery, там полно примеров фасада
// он нужен просто для того, чтоб увести нас от того,
// чтоб заострять внимание на сложностях проектирования и сделать проектирование быстрее и проще.

Паттерн фасад (facade) получил своё название из архитектуры. В архитектуре фасад - это, обычно, одна из внешних сторон здания, как правило - передняя сторона. Английский язык позаимствовал слово "facade" из французского. Речь идёт о слове "façade", которое, кроме прочего, переводится как "лицевая сторона здания".

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

Паттерн фасад можно использовать в огромном количестве ситуаций, среди которых можно особо отметить те из них, когда код пытаются сделать более простым для понимания (то есть - скрывают сложные механизмы за простыми API), и те, когда фрагменты систем стремятся сделать как можно слабее связанными друг с другом.

Несложно заметить, что объект-фасад (или слой с несколькими объектами) - это весьма полезная абстракция. Вряд ли кто-нибудь захочет столкнуться с драконом в том случае, если этого можно избежать. Объект-фасад нужен для того, чтобы предоставить другим объектам удобный API, а со всеми драконьими хитростями этот объект будет справляться самостоятельно.

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

Паттерн фасад часто можно встретить в Angular. Там сервисы используются как средства для упрощения некоей базовой логики. Но этот паттерн применим не только в Angular, ниже вы сможете в этом убедиться.

Предположим, нам нужно добавить в приложение систему управления состоянием. Для решения этой задачи можно воспользоваться разными средствами, среди них - Redux, NgRx, Akita, MobX, Apollo, а также - постоянно появляющиеся новые инструменты. Почему бы не испытать их все? Какова основная функциональность, которую должна предоставлять библиотека для управления состоянием? Вероятно, это - следующие возможности:

  • Механизм для оповещения системы управления состоянием о том, что нам нужно изменить состояние.
  • Механизм для получения текущего состояния или его фрагмента.

Выглядит всё это не так уж и плохо.

Теперь, вооружившись паттерном фасад, можно написать фасады для работы с различными частями состояния, предоставляющие удобные API, которыми можно пользоваться в программе. Например, нечто вроде facade.startSpinner(), facade.stopSpinner() и facade.getSpinnerState(). Подобные методы просто понять, на них легко можно сослаться в разговоре о программе.

После этого вы можете поработать с объектами, реализующими паттерн фасад и написать код, который будет трансформировать ваш код так, чтобы он мог бы работать с Apollo (управление состоянием с помощью GraphQL - горячая тема). Возможно, в ходе испытаний вы обнаружите, что Apollo вам не подходит, или то, что вам неудобно писать модульные тесты в расчёте на эту систему управления состоянием. Нет проблем - напишите новый фасад, рассчитанный на поддержку MobX, и снова испытайте систему.


Поведенческие шаблоны

Распределяют обязанности между объектами и тем, как они сообщаются.

Observer

Позволяет объектам наблюдать за объектами и быть оповещенными об изменениях.

Это шаблон проектирования, в котором объект, называемый субъектом (subject) поддерживает список зависимых объектов, называемых наблюдателями (observer), и автоматически уведомляет их при изменении своего состояния, обычно - вызывая один из их методов.

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

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

После того, когда вы, наконец, раздобудете нужную вам газету, вы сможете приступить к тому, к чему стремились всё это время - возьмёте чашечку кофе и развернёте газету. В JavaScript это было бы равносильно вызову коллбэка, который мы собирались вызвать после получения нужного результата.

Гораздо разумнее будет поступить так: подписаться на газету и каждый день получать её свежий выпуск. При таком подходе издатель даст вам знать о том, что вышел свежий номер газеты, и доставит его вам. Вам не придётся больше ходить в киоск. Не придётся больше тратить время впустую.

Если опять перейти к JavaScript, это означает, что вам больше не придётся ждать в цикле какого-то результата, и, получив его, вызывать некую функцию. Вместо этого вы сообщаете субъекту о том, что заинтересованы в неких событиях (сообщениях), и передаёте ему функцию обратного вызова, которая должна быть вызвана тогда, когда интересующие вас данные будут готовы. Вы, в таком случае, становитесь наблюдателем.

У рассматриваемого паттерна есть одна приятная особенность: вы не обязаны быть единственным наблюдателем. Если вы не сможете достать свою любимую газету - вас это расстроит. Но то же самое произойдёт и с другими людьми, которые не смогут её купить. Именно поэтому на одного субъекта могут подписываться несколько наблюдателей.

Паттерн наблюдатель применяется во многих ситуациях, но обычно его следует использовать тогда, когда вы хотите создать между объектами зависимость "один ко многим", и при этом такие объекты не должны быть сильно связанными. Кроме того, в создаваемой системе должна быть возможность оповещать неограниченное количество объектов о неких изменениях.

JavaScript-приложения - это отличное место для применения паттерна наблюдатель, так как здесь всё управляется событиями, и, вместо того, чтобы постоянно обращаться к некоей сущности, узнавая, произошло ли интересующее вас событие, гораздо лучше будет дать ей возможность оповестить вас при возникновении этого события (это похоже на старое высказывание: "Не надо нам звонить. Когда надо, мы сами вам позвоним").

Вполне вероятно, что вы уже пользовались конструкциями, напоминающими паттерн наблюдатель. Например - это addEventListener. Добавление к элементу прослушивателя событий имеет все признаки использования паттерна наблюдатель:

  • Вы можете подписаться на объект.
  • Вы можете отписаться от объекта.
  • Объект может информировать о событии всех своих подписчиков.

Знать о существовании паттерна наблюдатель полезно в том плане, что это знание позволяет вам реализовать собственный субъект, или гораздо быстрее, чем раньше, разобраться с существующим решением, использующим этот паттерн.

Базовая реализация этого паттерна не должна быть особенно сложной, но существуют отличные библиотеки, реализующие его и используемые во многих проектах. Речь идёт о проекте ReactiveX, и о его JavaScript-варианте RxJS.

Библиотека RxJS позволяет не только подписываться на субъекты, но и даёт программисту возможность трансформации данных самыми разными способами, позволяет комбинировать множество подписок, улучшает возможности по управлению асинхронными операциями. Кроме того, этим её возможности не ограничиваются. Если вы когда-нибудь хотели поднять возможности своих программ по обработке и преобразованию данных на более высокий уровень, то вам можно порекомендовать изучить библиотеку RxJS.

Mediator

Один объект контролирует сообщение между объектами, поэтому объекты не сообщаются друг с другом на прямую.

Command

Инкапсулирует вызов метода в один объект.

// Command Pattern

(function() {
	var carManager = {    
		// Запросить информацию
		requestInfo: function(model, id) {
			return "The information for " + model + " with ID " + id + " is foobar";
		},
		
		// Купить машину
		buyVehicle: function(model, id) {
			return "You have successfully purchased Item " + id + ", a " + model;
		},
		
		// Организовать просмотр
		arrangeViewing: function(model, id) {
			return "You have successfully booked a viewing of " + model + " ( " + id + " ) ";
		}
	};
})();

carManager.execute = function (name) {
	return carManager[name] && carManager[name].apply(carManager, [].slice.call(arguments, 1));
};

carManager.execute("arrangeViewing", "Ferrari", "14523");
carManager.execute("requestInfo", "Ford Mondeo", "54323");