Вы когда-нибудь работали в команде, когда вам нужно было начать разрабатывать проект с нуля? Такое обычно происходит во многих молодых и небольших компаниях.
Существует такое большое количество языков программирования, архитектур и других особенностей, что глаза разбегаются, и сложно определиться, с чего начать. И вот здесь на помощь приходят шаблоны проектирования.
Шаблон проектирования – это что-то вроде заготовки для вашего проекта. В нем соблюдаются определенные требования, и вы уже в принципе знаете, как он будет себя вести. Эти шаблоны были созданы на основе опыта большого количества разработчиков, именно поэтому их можно назвать разноплановыми наборами наработанных методик.
Вы и ваша команда можете выбрать тот набор, который больше всего подойдет для вашего проекта. Шаблон проектирования, который вы выберете, продиктует, что должен делать ваш код, и какой словарь вы будете использовать.
Шаблоны проектирования программ могут использоваться во всех языках программирования и для любого проекта, так как они предлагают только общую концепцию решения.
Есть 23 официальных шаблона, которые описаны в книге Design Patterns - Elements of Reusable Object-Oriented Software. Эта книга считается одной из самых авторитетных книг по объектно-ориентированной теории и разработке программного обеспечения.
В этой статье я расскажу только о четырех шаблонах проектирования, чтобы вы имели правильное представление о том, что такое эти шаблоны и когда их нужно использовать.
Шаблон проектирования Singleton (Одиночка)
Шаблон Одиночка позволяет классу или объекту иметь только один экземпляр, а для хранения этого экземпляра использует глобальную переменную. Вы можете использовать отложенной загрузкой для того, чтобы убедиться, что есть только один экземпляр класса, так как он будет создавать класс только тогда, когда в этом будет необходимость.
Это помогает избежать одновременного функционирования нескольких экземпляров, что может провоцировать возникновение фатальных ошибок. Чаще всего это реализуется в конструкторе. Целью шаблона, как правило, является регулирование глобального состояния приложения.
Примером шаблона Одиночка, который вы, с большей долей вероятности, постоянно используете, может быть ваш диспетчер протоколирования.
Если вы работаете с несколькими front-end фреймворками, такими как React или Angular, то вы, наверняка, знаете, как сложно обрабатывать журналы, которые приходят из разных компонентов приложения. Это отличный пример работы шаблона Singleton, так как вам никогда не нужно будет более одного экземпляра диспетчера протоколирования, особенно если вы используете какой-нибудь из инструментов отслеживания ошибок.
class FoodLogger {
constructor() {
this.foodLog = []
}
log(order) {
this.foodLog.push(order.foodItem)
// do fancy code to send this log somewhere
}
}
// this is the singleton
class FoodLoggerSingleton {
constructor() {
if (!FoodLoggerSingleton.instance) {
FoodLoggerSingleton.instance = new FoodLogger()
}
}
getFoodLoggerInstance() {
return FoodLoggerSingleton.instance
}
}
module.exports = FoodLoggerSingleton
Пример класса, использующего шаблон Singleton
Теперь вы не будете беспокоиться о том, что журналы из нескольких экземпляров могут потеряться, потому что в вашем проекте экземпляр будет только один. Поэтому, если вы хотите записать информацию о заказанной еде, то вы можете использовать один и тот же экземпляр FoodLogger для нескольких файлов или компонентов.
const FoodLogger = require('./FoodLogger')
const foodLogger = new FoodLogger().getFoodLoggerInstance()
class Customer {
constructor(order) {
this.price = order.price
this.food = order.foodItem
foodLogger.log(order)
}
// other cool stuff happening for the customer
}
module.exports = Customer
Пример класса Customer (покупатель), использующего шаблон Singleton
const FoodLogger = require('./FoodLogger')
const foodLogger = new FoodLogger().getFoodLoggerInstance()
class Restaurant {
constructor(inventory) {
this.quantity = inventory.count
this.food = inventory.foodItem
foodLogger.log(inventory)
}
// other cool stuff happening at the restaurant
}
module.exports = Restaurant
Если вы используете этот шаблон, вам не нужно беспокоится о том, что журналы можно получить только из основного файла приложения. Вы можете получить их из любой части вашей кодовой базы, и все они будут направлены в один и тот же экземпляр диспетчера протоколирования. Это значит, что ни один из ваших журналов не будет потерян из-за наличия других экземпляров.
Шаблон проектирования Strategy (Стратегия)
Стратегия – это шаблон, который похож на усовершенствованный вариант оператора if-else. Фактически, это то, где вы создаете интерфейс для метода, который есть в вашем родительском классе. После чего этот интерфейс используется для того, чтобы найти правильную реализации этого метода, которую вы будете использовать в дочернем классе. В данном случае реализация будет определяться клиентом в процессе выполнения.
Этот шаблон очень помогает в ситуациях, когда у вас в классе есть обязательные методы и методы по выбору. Некоторые экземпляры этого класса не нуждаются в методах по выбору, и это создает проблему для механизма наследования. Вы можете использовать интерфейсы для методов по выбору, но тогда каждый раз, когда вы будете использовать этот класс, вам придется писать реализацию, так как реализации по умолчанию не будет.
Здесь на помощь приходит шаблон Стратегия. Клиент, вместо того, чтобы искать реализацию, делегирует свои полномочия интерфейсу Стратегии, и уже она находит правильную реализацию. Одно из самых распространенных применений такого подхода - системы обработки платежей.
У вас может быть корзина для покупок, оплатить которую клиенты могут только с помощью кредитных карт, но вы упустите тех клиентов, которые хотят использовать другие способы оплаты.
Шаблон разработки Strategy позволяет разделить способы оплаты и процесс оформления заказа, а это значит, что мы можем добавлять или обновлять стратегии без изменения кода, который относится к корзине покупок или к процессу оформления заказа.
Ниже приведен пример реализации модели Стратегии на примере оплаты покупок.
class PaymentMethodStrategy {
const customerInfoType = {
country: string
emailAddress: string
name: string
accountNumber?: number
address?: string
cardNumber?: number
city?: string
routingNumber?: number
state?: string
}
static BankAccount(customerInfo: customerInfoType) {
const { name, accountNumber, routingNumber } = customerInfo
// do stuff to get payment
}
static BitCoin(customerInfo: customerInfoType) {
const { emailAddress, accountNumber } = customerInfo
// do stuff to get payment
}
static CreditCard(customerInfo: customerInfoType) {
const { name, cardNumber, emailAddress } = customerInfo
// do stuff to get payment
}
static MailIn(customerInfo: customerInfoType) {
const { name, address, city, state, country } = customerInfo
// do stuff to get payment
}
static PayPal(customerInfo: customerInfoType) {
const { emailAddress } = customerInfo
// do stuff to get payment
}
}
Для того, чтобы реализовать нашу стратегию способов оплаты, мы создали один класс с несколькими статическими методами. Каждый из методов принимает на вход один и тот же параметр, customerInfo, и этот параметр имеет тип customerInfoType. (Приветствую всех разработчиков TypeScript!) Здесь стоит обратить внимание на то, что каждый метод имеет свою собственную реализацию и использует разные значения из customerInfo.
Если вы используете шаблон Стратегия, то вы можете динамически менять стратегию, которая будет использоваться при выполнении программы. Это значит, что у вас есть возможность изменить используемую стратегию или реализацию метода в зависимости от входных данных пользователя или среды, в которой запускается приложение.
Вы можете установить реализацию по умолчанию в файле config.json:
{
"paymentMethod": {
"strategy": "PayPal"
}
}
Файл config.json для установки реализации paymentMethod по умолчанию на «PayPal»
Каждый раз, когда клиент начинает процесс оформления заказа на вашем веб-сайте, то он видит способ оплаты по умолчанию – PayPal, который был получен из файла config.json. Если клиенту нужен другой способ оплаты, его можно легко изменить, просто выбрав другой.
А теперь давайте создадим файл для процесса оформления заказа:
const PaymentMethodStrategy = require('./PaymentMethodStrategy')
const config = require('./config')
class Checkout {
constructor(strategy='CreditCard') {
this.strategy = PaymentMethodStrategy[strategy]
}
// do some fancy code here and get user input and payment method
changeStrategy(newStrategy) {
this.strategy = PaymentMethodStrategy[newStrategy]
}
const userInput = {
name: 'Malcolm',
cardNumber: 3910000034581941,
emailAddress: 'mac@gmailer.com',
country: 'US'
}
const selectedStrategy = 'Bitcoin'
changeStrategy(selectedStrategy)
postPayment(userInput) {
this.strategy(userInput)
}
}
module.exports = new Checkout(config.paymentMethod.strategy)
Класс Checkout — это то, где демонстрируется работа шаблона Стратегия. Мы импортируем два файла для того, чтобы у нас были стратегии способов оплаты, которые может выбрать клиент, и стратегия по умолчанию из файла config.
Далее мы создаем класс с конструктором и резервным значением для стратегии по умолчанию на случай, если в файле config не было ни одного набора. После чего мы присваиваем значение стратегии локальной переменной состояния.
Важно реализовать в нашем классе Checkout возможность изменения стратегии оплаты. Клиент может изменить способ оплаты на тот, который он хочет использовать, и вы должны быть в состоянии обработать это. Именно для этого нужен метод changeStrategy.
После того, как вы удачно все это запрограммировали и получили все входные данные от клиента, то вы сразу же можете обновить стратегию оплаты, опираясь на его ввод, и шаблон динамически установит стратегию перед отправкой платежа в обработку.
Рано или поздно вам может потребоваться добавить еще какие-нибудь способы оплаты, и все, что вам нужно сделать, это просто добавить их в класс PaymentMethodStrategy. И этот способ оплаты станет доступным везде, где используется этот класс.
Шаблон проектирования Strategy - это мощный инструмент, который активно помогает, когда вы имеете дело с методами, у которых есть несколько реализаций. У вас могут возникнуть ощущения, что вы используете интерфейс, но вам не нужно писать реализацию для метода каждый раз, когда вы вызываете его в другом классе. Такой подход дает вам больше гибкости, чем интерфейсы.
Шаблон проектирования Observer (Наблюдатель)
Если вы когда-нибудь использовали шаблон проектирования «модель-представление-контроллер» (MVC), то вы также использовали шаблон проектирования Наблюдатель. Модель похожа на субъект, а Представление – на наблюдателя за этим субъектом. Субъект хранит все данные и состояние этих данных. И у вас есть наблюдатели – различные компоненты, которые будут получать эти данные от субъекта при их обновлении.
Цель шаблона проектирования Наблюдатель – создать отношения «один ко многим» между субъектом и всеми наблюдателями, которые ждут данные для обновления. Таким образом, как только произойдет изменение состояния объекта, все наблюдатели будут мгновенно уведомлены и смогут обновить данные.
К примерам использования такого шаблона можно отнести следующее: отправка уведомлений пользователям, обновления, фильтры и управление подписчиками.
Допустим, что у вас есть одностраничное приложение с тремя раскрывающимися списками, которые зависят от выбора категории из раскрывающегося списка уровнем выше. Такой подход часто используется на многих сайтах электронных продаж, таких как Home Depot. У вас есть большое количество фильтров на странице, которые зависят от значения фильтра уровнем выше.
Код раскрывающегося списка верхнего уровня может выглядит вот так:
class CategoryDropdown {
constructor() {
this.categories = ['appliances', 'doors', 'tools']
this.subscriber = []
}
// pretend there's some fancy code here
subscribe(observer) {
this.subscriber.push(observer)
}
onChange(selectedCategory) {
this.subscriber.forEach(observer => observer.update(selectedCategory))
}
}
Файл CategoryDropdown - это просто класс с конструктором, который инициализирует параметры категории, которые можно выбрать в раскрывающемся списке. Это файл, который вы будете обрабатывать, прежде чем пользователь увидит параметры. Мы можете выбирать списки из серверной части или выполнять любую сортировку.
Метод subscribe отвечает за то, что каждый фильтр, который был создан с помощью этого класса, должен получать обновления о состоянии наблюдателя.
Метод onChange отвечает за отправку всем подписчикам уведомлений о том, что в наблюдателе, за которым они следят, произошло изменение состояния. Мы просто перебираем всех подписчиков и вызываем их метод update с параметром selectedCategory.
Код для других фильтров может выглядеть следующим образом:
class FilterDropdown {
constructor(filterType) {
this.filterType = filterType
this.items = []
}
// more fancy code here; maybe make that API call to get items list based on filterType
update(category) {
fetch('https://example.com')
.then(res => this.items(res))
}
}
Файл FilterDropdown – это еще один простой класс, который описывает все потенциальные раскрывающиеся списки, которые мы могли бы использовать на странице. При создании нового экземпляра этого класса, ему нужно передать в качестве параметра filterType. Его можно использовать для выполнения определенных вызовов API, чтобы получить список элементов.
Метод update - это реализация того, что вы можете делать с новой категорией после того, как он была отправлена из наблюдателя.
А теперь посмотрим, что получается, если использовать эти файлы с шаблоном Наблюдатель:
const CategoryDropdown = require('./CategoryDropdown')
const FilterDropdown = require('./FilterDropdown')
const categoryDropdown = new CategoryDropdown()
const colorsDropdown = new FilterDropdown('colors')
const priceDropdown = new FilterDropdown('price')
const brandDropdown = new FilterDropdown('brand')
categoryDropdown.subscribe(colorsDropdown)
categoryDropdown.subscribe(priceDropdown)
categoryDropdown.subscribe(brandDropdown)
Этот файл говорит нам о том, что у нас есть три раскрывающихся списка, которые выступают в роли подписчиков раскрывающегося списка категорий, за которым они наблюдают. Далее каждый из этих списков мы подписываем на наблюдателя. Каждый раз, когда категория наблюдателя обновляется, он отправляем это значение каждому подписчику, после чего они мгновенно обновляют отдельные раскрывающиеся списки.
Шаблон проектирования Decorator (Декоратор)
Шаблон проектирования Декоратор довольно прост в использовании. У вас может быть базовый класс с методами и свойствами, и с его помощью вы можете создать новый объект. А теперь предположим, то у вас есть несколько экземпляров класса, для которых требуются какие-то методы или свойства, которых в базовом классе нет.
Вы можете добавить эти методы и свойства в ваш базовый класс, но это может негативно повлиять на другие ваши экземпляры. Вы даже можете создать подклассы для того, чтобы хранить определенные методы и свойства, которые вам нужны, но которые по каким-то причинам вы не можете добавить в базовый класс.
Любой их этих подходов в принципе может решить вашу проблему, но они очень корявые и неэффективные. Вот здесь и вступает в игру шаблон Декоратор. Вместо того, чтобы портить свою кодовую базу, дабы добавить несколько вещей лишь к одному экземпляру, вы можете добавить их непосредственно к конкретному экземпляру.
Поэтому, если вам нужно добавить новое свойство - цену объекта, вы можете использовать шаблон Декоратор для того, чтобы добавить его непосредственно к этому конкретному экземпляру объекта, и это никак не повлияет на другие его экземпляры.
Если вы когда-нибудь заказывали еду через интернет, то, скорее всего, вы уже сталкивались с шаблоном Декоратор. Если вы хотите заказать сэндвич и при этом хотите добавить туда дополнительные ингредиенты, то веб-сайт не будет добавлять их к каждому текущему экземпляру сэндвича, который пытаются заказать текущие пользователи.
Вот пример класса Customer (покупатель):
class Customer {
constructor(balance=20) {
this.balance = balance
this.foodItems = []
}
buy(food) {
if (food.price) < this.balance {
console.log('you should get it')
this.balance -= food.price
this.foodItems.push(food)
}
else {
console.log('maybe you should get something else')
}
}
}
module.exports = Customer
А вот пример класса Sandwich (сэндвич):
class Sandwich {
constructor(type, price) {
this.type = type
this.price = price
}
order() {
console.log(`You ordered a ${this.type} sandwich for $ ${this.price}.`)
}
}
class DeluxeSandwich {
constructor(baseSandwich) {
this.type = `Deluxe ${baseSandwich.type}`
this.price = baseSandwich.price + 1.75
}
}
class ExquisiteSandwich {
constructor(baseSandwich) {
this.type = `Exquisite ${baseSandwich.type}`
this.price = baseSandwich.price + 10.75
}
order() {
console.log(`You ordered an ${this.type} sandwich. It's got everything you need to be happy for days.`)
}
}
module.exports = { Sandwich, DeluxeSandwich, ExquisiteSandwich }
Пример класса Sandwich
В классе Sandwich используется шаблон Декоратор. Есть базовый класс Sandwich, который устанавливает некие правила для заказа обычного сэндвича. Клиенты могут захотеть изменить его, а это значит, что изменятся ингредиенты и цена.
Вам нужно просто добавить функцию для того, чтобы увеличить цену и обновить тип сэндвича на DeluxeSandwich, не меняя при этом порядок его заказа. Или вы можете добаить еще один метод, чтобы заказать ExquisiteSandwich, так как там качество ингредиентов будет совсем иное.
Шаблон Декоратор позволяет динамически менять базовый класс, не влияя на него и любые другие классы. Вам не нужно беспокоиться о реализации функций, которых вы не понимаете, например, интерфейсы, и вам не нужно добавлять свойства, которые не будут использоваться в каждом классе.
А теперь рассмотрим пример, в котором этот класс создается так, чтобы клиент мог заказать сэндвич.
const { Sandwich, DeluxeSandwich, ExquisiteSandwich } = require('./Sandwich')
const Customer = require('./Customer')
const cust1 = new Customer(57)
const turkeySandwich = new Sandwich('Turkey', 6.49)
const bltSandwich = new Sandwich('BLT', 7.55)
const deluxeBltSandwich = new DeluxeSandwich(bltSandwich)
const exquisiteTurkeySandwich = new ExquisiteSandwich(turkeySandwich)
cust1.buy(turkeySandwich)
cust1.buy(bltSandwich)
Заключение
Раньше я считал, что шаблоны проектирования – это какие-то бредовые и далекие от реальности принципы разработки программного обеспечения. А потом я вдруг узнал, что пользуюсь ими постоянно!
Некоторые из рассмотренных нами шаблонов используются в таком количестве приложений, что вам и не снилось. В конце концов, это всего лишь теория. А мы, разработчики, должны использовать эту теорию для того, чтобы улучшить наши приложения, а именно сделать их более простыми с точки зрения реализации и сопровождения.