Это универсальное средство для освоения шаблонов проектирования. А если вы торопитесь, я подготовил несколько кратких резюме, которые произведут впечатление на любого интервьюера быстрее, чем вы успеете сказать: «Боб — ваш дядя».

Давным-давно, недалеко от рая для разработчиков, двум разработчикам поручили создать приложение. Один был опытным ветераном программирования, а другой был новичком в мире программирования. Что может пойти не так?

Опытный разработчик считал, что шаблоны проектирования предназначены для слабаков. «Кому нужна вся эта причудливая болтовня, когда у тебя за плечами годы опыта?» они издевались. Они с головой погрузились в проект, не задумываясь дважды о шаблонах проектирования.

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

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

Тем временем новый разработчик быстро справлялся со своими задачами. У них была модульная система, которую было легко понять и модифицировать. Они могли добавлять новые функции, не нарушая существующие, и все работало как по маслу. «Это слишком просто», — подумали они.

В конце концов, код опытного разработчика содержал ошибки, сложен в обслуживании и требовал постоянного внимания. С другой стороны, код нового разработчика был гладким, как попка младенца.
Мораль этой истории?

Не будьте героем, используйте шаблоны проектирования. Ваше будущее скажет вам спасибо.

Внимание всем любителям программного обеспечения! Приготовьтесь к дикой поездке по основным шаблонам проектирования, которые вам нужно знать, потому что, давайте посмотрим правде в глаза, кто хочет застрять в каменном веке разработки программного обеспечения? Эта статья — универсальное пособие для освоения этих важнейших концепций. А если вы спешите, я набросал несколько кратких резюме, которые произведут впечатление на любого интервьюера быстрее, чем вы успеете сказать: «Боб — ваш дядя». момент, чтобы отдышаться, налить себе чашку кофе и подготовиться к творчеству!

Хотя примеры кода в этой статье написаны с использованием JavaScript, не стесняйтесь вносить свой вклад или исследовать этот репозиторий и для других языков программирования.

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

1. Структурные модели

Структурные паттерны подобны строительным лесам корабля, скрепляя все вместе и обеспечивая структуру. Они помогают разработчикам организовывать свой код таким образом, чтобы его было легко понять и модифицировать, подобно тому, как различные части корабля устроены так, чтобы он функционировал должным образом.
Без прочного киля корабль будет раскачивать волнами и он не сможет эффективно ориентироваться. Точно так же без структурных шаблонов код может стать неорганизованным и сложным в обслуживании, что затруднит разработку новых функций или исправление ошибок. Независимо от того, строите ли вы маленькую лодку или массивный океанский лайнер, для успеха необходим прочный каркас!

1–1. Адаптер

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

Как:

  • Определите несовместимые интерфейсы между двумя классами.
  • Создайте класс адаптера, который реализует интерфейс одного класса и делегирует вызовы другому классу.
  • Измените клиентский код, чтобы использовать класс адаптера вместо несовместимого класса.

Почему:

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

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

class Heisenberg {
    sayMyName() {
      console.log("You're goddamn right.");
    }
  }
  
  // The Target interface
  class DrugDealerInterface {
    identifyYourself() {
      throw new Error('This method must be overridden!');
    }
  }
  
  // The Adapter class
  class DrugDealerAdapter extends DrugDealerInterface {
    constructor(adaptee) {
      super();
      this.adaptee = adaptee;
    }
  
    identifyYourself() {
      this.adaptee.sayMyName();
    }
  }
  
  // Usage
  const heisenberg = new Heisenberg();
  const drugDealer = new DrugDealerAdapter(heisenberg);
  drugDealer.identifyYourself(); // Output: "You're goddamn right."

В этом примере класс Heisenberg представляет Adaptee, у которого есть метод sayMyName(), который мы хотим адаптировать для соответствия DrugDealerInterface. Мы создаем класс Adapter с именем DrugDealerAdapter, который расширяет класс DrugDealerInterface и содержит экземпляр объекта Heisenberg. DrugDealerAdapter адаптирует метод sayMyName() объекта Heisenberg к методу identifyYourself() объекта DrugDealerInterface.

Наконец, когда мы вызываем метод identifyYourself() для нашего объекта DrugDealerAdapter, он выводит знаменитую цитату Уолтера Уайта: «Ты чертовски прав».

1–2. Фасад

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

Как:

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

Почему:

  • Предоставляет упрощенный интерфейс для сложной подсистемы.
  • Уменьшает связь между клиентским кодом и подсистемой.
  • Может повысить производительность за счет уменьшения количества вызовов между клиентским кодом и подсистемой.

Пример. В Super Mario Bros Марио приходится перемещаться по разным уровням, заполненным препятствиями и врагами. Он может собирать бонусы, такие как грибы и огненные цветы, чтобы получить дополнительные способности, но игрокам может быть трудно запомнить, какие бонусы делают что и когда их использовать. Это может привести к разочарованию и путанице. Мы можем создать класс PowerUpFacade, который предоставляет методы для поедания различных бонусов и обрабатывает логику того, что происходит, когда съедается каждый бонус.

class Mushroom {
  constructor() {
    this.name = 'Mushroom';
  }

  eat() {
    console.log('Yum! Grew bigger!');
  }
}

class FireFlower {
  constructor() {
    this.name = 'Fire Flower';
  }

  eat() {
    console.log('Awesome! Can shoot fireballs now!');
  }
}

class Star {
  constructor() {
    this.name = 'Star';
  }

  eat() {
    console.log('Woo-hoo! Invincible!');
  }
}

class Mario {
  constructor(name) {
    this.name = name;
  }

  jump() {
    console.log(`Jumped as ${this.name}`);
  }

  run() {
    console.log(`Ran as ${this.name}`);
  }
}

class PowerUpFacade {
  constructor(mushroom, fireFlower, star, mario) {
    this.mushroom = mushroom;
    this.fireFlower = fireFlower;
    this.star = star;
    this.mario = mario;
  }

  eatMushroom() {
    this.mushroom.eat();
    this.mario.jump();
  }

  eatFireFlower() {
    this.fireFlower.eat();
    this.mario.run();
  }

  eatStar() {
    this.star.eat();
    this.mario.jump();
    this.mario.run();
  }
}

// Usage example
const mushroom = new Mushroom();
const fireFlower = new FireFlower();
const star = new Star();
const mario = new Mario('Mario');

const powerUpFacade = new PowerUpFacade(mushroom, fireFlower, star, mario);

powerUpFacade.eatMushroom(); // Yum! Grew bigger! Jumped as Mario
powerUpFacade.eatFireFlower(); // Awesome! Can shoot fireballs now! Ran as Mario
powerUpFacade.eatStar(); // Woo-hoo! Invincible! Jumped as Mario, Ran as Mario

В этом примере у нас есть класс PowerUpFacade, который действует как упрощенный интерфейс к базовой системе классов и интерфейсов для управления бонусами в Super Mario Bros. Фасадный класс предоставляет методы для поедания грибов, огненных цветов и звезд, которые дают Марио обладает различными способностями, такими как рост, стрельба огненными шарами и непобедимость.

1–3. Композит (бекон к моим яйцам!)

Обзор. Шаблон Composite — это шаблон проектирования, который позволяет единообразно обрабатывать отдельные объекты и группы объектов путем создания иерархии классов, в которой оба типа объектов рассматриваются как объекты-компоненты с общим интерфейсом. Этот шаблон упрощает код и повышает гибкость, позволяя клиентам работать с объектами согласованным образом, независимо от того, являются ли они однолистовыми объектами или составными групповыми объектами.

Как:

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

Почему:

  • Позволяет клиентам единообразно обрабатывать отдельные объекты и композиции.
  • Упрощает клиентский код, предоставляя единый интерфейс для всех объектов.
  • Включает рекурсивную композицию объектов, создавая древовидные структуры.

Пример. Допустим, у нас есть группа, играющая метал, и мы хотим представить различные части песни с помощью шаблона Composite. Мы можем начать с базового класса Instrumental, в котором есть метод play(). Это представляет собой общий интерфейс, который будет использоваться всеми инструменталами. Затем мы можем создать несколько конечных классов, представляющих отдельные инструменталы, такие как Guitar, Drums и Bass. Наконец, мы можем создать составной класс с именем Song , представляющий полную песню. Класс Song может содержать несколько экземпляров Instrumental (которые могут быть листом или другим составным объектом).

Вот пример фрагмента кода:

class Instrumental {
  play() {
    return '...';
  }
}

class Guitar extends Instrumental {
  play() {
    return '🎸🎵🤘';
  }
}

class Drums extends Instrumental {
  play() {
    return '🥁🎵🤘';
  }
}

class Bass extends Instrumental {
  play() {
    return '🎸(bass)🎵🤘';
  }
}

class Song extends Instrumental {
  constructor() {
    super();
    this.instrumentals = [];
  }

  addInstrumental(instrumental) {
    this.instrumentals.push(instrumental);
  }

  play() {
    let song = '';
    this.instrumentals.forEach((instrumental) => {
      song += `${instrumental.play()} `;
    });
    return song;
  }
}

// Usage:
const guitar = new Guitar();
const drums = new Drums();
const bass = new Bass();

const verse = new Song();
verse.addInstrumental(guitar);
verse.addInstrumental(drums);
verse.addInstrumental(bass);

const chorus = new Song();
chorus.addInstrumental(guitar);
chorus.addInstrumental(drums);

const song = new Song();
song.addInstrumental(verse);
song.addInstrumental(chorus);

console.log(song.play()); // Output: 🎸🎵🤘 🥁🎵🤘 🎸(bass)🎵🤘 🎸🎵🤘 🥁🎵🤘 

В этом примере мы представляем металлическую песню, используя шаблон Composite, создавая конечные классы для отдельных инструментальных композиций и составной класс для всей песни. Затем мы добавляем к песне различные комбинации инструменталов, чтобы создать разные части песни, такие как куплет и припев, и воспроизводим всю песню, вызывая метод play() для объекта Song верхнего уровня.

1–4. Прокси

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

Как:

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

Почему:

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

Пример. Представьте, что вы командуете армией викингов и хотите, чтобы только авторизованный персонал мог получить доступ к вашему объекту армии. Вы решаете использовать шаблон Proxy для создания Viking Army Proxy, который контролирует доступ к реальному объекту Viking Army.

const VikingArmy = {
  warriors: ['Ragnar', 'Lagertha', 'Bjorn'],
  weapons: ['axe', 'sword', 'spear']
}

const VikingArmyProxy = new Proxy(VikingArmy, {
  get: function(target, property) {
    if (property === 'weapons') {
      console.log('Access denied! Only authorized Vikings can access the weapons.');
      return [];
    } else {
      return target[property];
    }
  }
});

// Unauthorized Viking tries to access weapons
console.log(VikingArmyProxy.weapons);
// Output: Access denied! Only authorized Vikings can access the weapons.
// []

// Authorized Viking accesses warriors
console.log(VikingArmyProxy.warriors);
// Output: [ 'Ragnar', 'Lagertha', 'Bjorn' ]

В этом примере мы создали объект VikingArmy с двумя свойствами: warriors и weapons. Затем мы создаем VikingArmyProxy с помощью new Proxy(), который перехватывает любые попытки доступа к свойству weapons и запрещает доступ, если Viking не авторизован.

1–5. Наилегчайший вес

Вы можете пропустить это!

Обзор.Шаблон приспособленца – это шаблон проектирования, целью которого является минимизация использования памяти приложением за счет совместного использования максимально возможного объема данных между несколькими объектами. Это достигается путем разделения внутренних и внешних состояний объекта — внутреннее состояние является общим для объектов, тогда как внешнее состояние может различаться между ними. Делая это, мы уменьшаем количество отдельных объектов, необходимых в приложении, и, таким образом, уменьшаем объем памяти. Шаблон Flyweight особенно полезен при работе с большим количеством похожих объектов, например, в графических приложениях или приложениях пользовательского интерфейса.

Как:

  • Определите данные, которые могут быть разделены между несколькими объектами.
  • Разделите внутреннее состояние (общие данные) и внешнее состояние (уникальные данные).
  • Реализуйте фабрику-приспособленец, которая управляет созданием и совместным использованием объектов-приспособленцев.

Почему:

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

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

Тем не менее, в команде всегда есть один игрок с подстановочным знаком — давайте назовем его «Боб». У Боба есть привычка менять свою позицию в середине игры, что затрудняет повторное использование объекта-легковеса.
В этом сценарии вам нужно будет создавать новый экземпляр объекта игрока специально для Боба каждый раз, когда он меняет позицию. Вы даже можете отправить ему специальное сообщение, например: «Боб снова вышел из-под контроля и сменил позицию. Создание нового объекта игрока только для него…».

// Define the flyweight object for player properties that are shared among multiple players
const playerFlyweight = {
  age: 25,
  height: 180,
  weight: 75
};

// Define the player class which accepts unique properties as parameters
class Player {
  constructor(name, position, skills) {
    this.name = name;
    this.position = position;
    this.skills = skills;
  }
  
  // A method to change Bob's position mid-game
  changePosition(newPosition) {
    if (this.name === 'Bob') {
      console.log('Bob has flown off the handle again and changed positions. Creating a new player object just for him...');
      
      // Create a new player object specifically for Bob with all unique properties passed in as parameters
      const bobPlayer = new Player('Bob', newPosition, ['Wild Card']);
      return bobPlayer;
    }
    
    // For all other players, reuse the flyweight object for common properties and only pass in unique properties as parameters
    return { ...playerFlyweight, name: this.name, position: newPosition, skills: this.skills };
  }
}

// Define an array of players on the team
const teamPlayers = [
  new Player('Alice', 'Forward', ['Speed', 'Precision']),
  new Player('Bob', 'Midfield', ['Unpredictability']),
  new Player('Charlie', 'Defense', ['Strength', 'Tackling']),
  new Player('Dave', 'Forward', ['Agility', 'Shooting'])
];

// Use the flyweight object for common properties and only pass in unique properties as parameters when creating a new player object
console.log(teamPlayers[0].changePosition('Left Forward')); // { age: 25, height: 180, weight: 75, name: 'Alice', position: 'Left Forward', skills: ['Speed', 'Precision'] }
console.log(teamPlayers[1].changePosition('Right Midfield')); // Bob has flown off the handle again and changed positions. Creating a new player object just for him...
console.log(teamPlayers[2].changePosition('Center Defense')); // { age: 25, height: 180, weight: 75, name: 'Charlie', position: 'Center Defense', skills: ['Strength', 'Tackling'] }
console.log(teamPlayers[3].changePosition('Right Forward')); // { age: 25, height: 180, weight: 75, name: 'Dave', position: 'Right Forward', skills: ['Agility', 'Shooting'] }

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

Когда игрок меняет позицию в середине игры, вызывается метод changePosition. Если игрок — Боб, специально для него создается новый объект игрока со всеми уникальными свойствами, передаваемыми в качестве параметров. Для всех остальных игроков мы повторно используем объект легковеса для общих свойств и передаем только уникальные свойства в качестве параметров при создании нового объекта игрока.

2. Модели поведения

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

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

2–1. Наблюдатель

Вывод:Шаблон Observer устанавливает зависимость "один ко многим" между объектами. Когда состояние одного объекта изменяется, все его иждивенцы уведомляются и обновляются автоматически. Это способствует слабой связи и обеспечивает гибкость при проектировании системы.

Как:

  • Определите субъектный интерфейс, который включает методы добавления, удаления и уведомления наблюдателей.
  • Реализуйте субъектный интерфейс в конкретном субъектном классе, который поддерживает список своих наблюдателей.
  • Определите интерфейс наблюдателя, который включает метод обновления своего состояния.
  • Реализуйте интерфейс наблюдателя в одном или нескольких конкретных классах наблюдателя.
  • Когда состояние субъекта изменится, вызовите метод notify() для всех его наблюдателей.

Почему:

  • Обеспечивает слабую связь между объектами, обеспечивая гибкость и расширяемость.
  • Устраняет необходимость в том, чтобы объекты явно знали друг о друге, что приводит к более модульной и удобной в сопровождении конструкции.
  • Упрощает обработку событий, автоматически уведомляя заинтересованные стороны о любых изменениях состояния объекта.

Пример. Проблема, которую мы пытаемся решить, заключается в том, как уведомить всех families (наблюдателей), когда Don Corleone (субъект) принимает решение.

// Define the Don Corleone (subject)
class DonCorleone {
  constructor() {
    this.families = []; // List of families to notify
  }

  registerFamily(family) { // Add family to the list
    this.families.push(family);
  }

  makeDecision(decision) { // Make a decision and notify all families
    console.log(`Don Corleone has made a decision: ${decision}`);
    this.families.forEach(function(family) {
      family.notify(decision);
    });
  }
}

// Define the families (observers)
class BarziniFamily {
  notify(decision) {
    console.log(`Barzini Family received the decision: ${decision}`);
  }
}
class TattagliaFamily {
  notify(decision) {
    console.log(`Tattaglia Family received the decision: ${decision}`);
  }
}
class CorleoneFamily {
  notify(decision) {
    console.log(`Corleone Family received the decision: ${decision}`);
  }
}

// Usage example
const donCorleone = new DonCorleone();
const barzini = new BarziniFamily();
const tattaglia = new TattagliaFamily();
const corleone = new CorleoneFamily();

donCorleone.registerFamily(barzini);
donCorleone.registerFamily(tattaglia);
donCorleone.registerFamily(corleone);

donCorleone.makeDecision("Leave the gun. Take the cannoli.");

В этом примере класс DonCorleone является субъектом, который принимает решения и ведет список семей (наблюдателей) для уведомления. Метод registerFamily используется для добавления новых семейств в список наблюдателей. Метод makeDecision вызывается, когда Дон Корлеоне принимает решение, и уведомляет все семьи в списке, вызывая их метод notify.

Классы BarziniFamily, TattagliaFamily и CorleoneFamily являются наблюдателями, у которых есть метод notify для получения и обработки решения, принятого Доном Корлеоне.

2–2. Команда

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

Как:

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

Почему:

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

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

// Define the command interface
class Command {
  execute() {}
}

// Concrete Commands
class MarchCommand extends Command {
  constructor(receiver) {
    super();
    this.receiver = receiver;
  }

  execute() {
    this.receiver.march();
  }
}

class AttackCommand extends Command {
  constructor(receiver) {
    super();
    this.receiver = receiver;
  }

  execute() {
    this.receiver.attack();
  }
}

// Receiver
class Soldier {
  march() {
    console.log("Soldier marching!");
  }

  attack() {
    console.log("Soldier attacking!");
  }
}

// Invoker
class Dictator {
  setCommand(command) {
    this.command = command;
  }

  giveOrders() {
    console.log("Dictator giving orders:");
    this.command.execute();
  }
}

// Usage
const dictator = new Dictator();
const soldier = new Soldier();

dictator.setCommand(new MarchCommand(soldier));
dictator.giveOrders(); // Output: "Dictator giving orders: Soldier marching!"

dictator.setCommand(new AttackCommand(soldier));
dictator.giveOrders(); // Output: "Dictator giving orders: Soldier attacking!"

В этом примере интерфейс Command определяет метод execute(), который должен быть реализован конкретными классами команд. Классы MarchCommand и AttackCommand представляют собой конкретные команды, которые выполняют определенные действия над объектом-приемником Soldier. Dictator действует как вызывающий, устанавливая команду и отдавая приказы подчиненным. Когда вызывается метод giveOrders(), инициатор вызывает метод execute() команды, в результате чего требуемые действия выполняются на получателе.

2–3. Состояние

Обзор.Шаблон состояния позволяет объекту изменять свое поведение при изменении его внутреннего состояния. Это устраняет необходимость в больших условных операторах, основанных на состоянии объекта, делая код более модульным и удобным в сопровождении. Шаблон включает в себя определение набора состояний и реализацию методов для обработки перехода между ними.

Как:

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

Почему:

  • Позволяет объекту изменять свое поведение при изменении его внутреннего состояния.
  • Устраняет необходимость в больших условных операторах, основанных на состоянии объекта.
  • Делает код более модульным и удобным в сопровождении.
  • Инкапсулирует поведение, зависящее от состояния, в отдельные классы.
  • Упрощает тестирование, упрощая изолированное тестирование каждой реализации состояния.

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

// Define a set of states that the vending machine can be in
class VendingMachineState {
  constructor(itemName) {
    this.itemName = itemName;
  }

  display() {
    console.log(`Vending machine is currently dispensing ${this.itemName}`);
  }
}

// Concrete state classes
class CandyState extends VendingMachineState {
  constructor() {
    super("candy");
  }
}

class SodaState extends VendingMachineState {
  constructor() {
    super("soda");
  }
}

class ChipsState extends VendingMachineState {
  constructor() {
    super("chips");
  }
}

// Context object - vending machine
class VendingMachine {
  constructor() {
    this.currentState = null;
    this.states = [new CandyState(), new SodaState(), new ChipsState()];
  }

  setState(state) {
    this.currentState = state;
  }

  getCurrentState() {
    return this.currentState;
  }
}

// Usage
const vendingMachine = new VendingMachine();

vendingMachine.setState(new SodaState());
vendingMachine.getCurrentState().display(); // Output: "Vending machine is currently dispensing soda"

vendingMachine.setState(new ChipsState());
vendingMachine.getCurrentState().display(); // Output: "Vending machine is currently dispensing chips"

В этом примере VendingMachineState — это абстрактный базовый класс, который определяет метод для отображения текущего предмета, выдаваемого торговым автоматом. Конкретные классы состояний — это CandyState, SodaState и ChipsState, которые наследуются от класса VendingMachineState. Класс VendingMachine — это объект контекста, который поддерживает ссылку на текущее состояние и список доступных состояний.

Когда состояние торгового автомата изменяется, мы вызываем setState() для объекта VendingMachine и передаем соответствующий объект состояния. Затем мы можем получить текущее состояние с помощью getCurrentState() и вызвать метод display(), чтобы показать выдаваемый предмет.

2–4. Цепочка ответственности

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

Как:

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

Почему:

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

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

// Define a chain of team members to handle bugs/issues
class TeamMember {
  constructor(name, expertise, nextMember) {
    this.name = name;
    this.expertise = expertise;
    this.nextMember = nextMember;
  }

  handleIssue(issue) {
    if (issue.type === this.expertise) {
      console.log(`${this.name} is handling ${issue.description}`);
      issue.resolved = true;
    } else if (this.nextMember) {
      console.log(`${this.name} cannot handle ${issue.description}, passing it on to ${this.nextMember.name}`);
      this.nextMember.handleIssue(issue);
    } else {
      console.log(`No one in the team can handle ${issue.description}!`);
      issue.resolved = false;
    }
  }
}

// Context object - the project with its issues
class Project {
  constructor() {
    this.issues = [
      { description: "UI not responsive", type: "frontend" },
      { description: "Database connection error", type: "backend" },
      { description: "Security vulnerability", type: "security" },
      { description: "Broken functionality", type: "frontend" }
    ];

    this.team = new TeamMember("Alice", "frontend", new TeamMember("Bob", "backend", new TeamMember("Charlie", "security", null)));
  }

  resolveIssues() {
    this.issues.forEach(issue => {
      console.log(`Handling ${issue.description}`);
      this.team.handleIssue(issue);
      console.log(`Issue resolved: ${issue.resolved}\n`);
    });
  }
}

// Usage
const project = new Project();
project.resolveIssues();

В этом примере класс TeamMember является абстрактным базовым классом, который определяет метод для обработки проблем. У каждого конкретного класса членов команды (Alice, Bob и Charlie) есть своя область знаний.

Класс Project — это объект контекста, который поддерживает список проблем для проекта и ссылку на первого члена команды в цепочке. Когда возникает проблема, мы вызываем handleIssue() у первого члена команды и передаем объект задачи. Если член команды может справиться с проблемой, он устраняет проблему и устанавливает для свойства resolved значение true. Если член команды не может справиться с проблемой, он передает ее следующему члену команды в цепочке. Если ни один член команды не может справиться с проблемой, она остается нерешенной.

Когда мы вызываем resolveIssues() для объекта Project, он перебирает список проблем и вызывает handleIssue() для первого члена команды для каждой проблемы. Вывод консоли показывает, какой член команды обрабатывает каждую проблему и была ли она успешно решена.

2–5. Сувениры

Вы можете пропустить это!

Обзор. Шаблон Memento — это поведенческий шаблон проектирования, который позволяет объекту сохранять и восстанавливать свое внутреннее состояние, не нарушая инкапсуляцию. Он включает в себя три основных компонента: создатель, который создает и сохраняет сувениры, реликвия, в которой хранится состояние создателя, и хранитель, который управляет сувенирами и восстанавливает состояние создателя по мере необходимости.

Как:

  • Определите класс Originator, который создает и сохраняет сувениры.
  • Определите класс Memento, в котором хранится состояние создателя.
  • Определите класс Caretaker, который управляет сувенирами и восстанавливает состояние создателя по мере необходимости.
  • Когда создателю необходимо сохранить свое состояние, он создает новый объект Memento и передает его состояние объекту Memento.
  • Memento сохраняет состояние и возвращает ссылку на себя Первоисточнику.
  • Создатель передает ссылку на память Хранителю на хранение.
  • Когда Создателю необходимо восстановить свое состояние, он просит Смотрителя предоставить соответствующий объект Memento.
  • Смотритель забирает запрошенный сувенир и передает его Создателю.
  • Создатель использует сувенир, чтобы восстановить его предыдущее состояние.

Почему:

  • Паттерн Memento используется для инкапсуляции и сохранения внутреннего состояния объекта.
  • Это позволяет восстановить состояние позже, не раскрывая деталей его реализации.
  • Этот шаблон полезен в ситуациях, когда необходимо отменить или повторить операцию.
  • Это также полезно при проверке состояния приложения в определенные моменты.
  • Используя шаблон Memento, вы можете гарантировать, что сохраненное состояние сохраняется в отдельном объекте и что исходный объект остается неизменным.
  • Этот шаблон поддерживает несколько контрольных точек и позволяет пользователям вернуться к любому ранее сохраненному состоянию.
  • Он предоставляет гибкое и мощное решение для управления состоянием в сложных приложениях.

Пример: как позволить «голодному монстру» сохранять и восстанавливать свое состояние, когда он ест разные виды пищи!

// Define the hungry monster (originator)
class HungryMonster {
  constructor() {
    this.stomach = []; // List of foods eaten by the monster
  }

  eat(food) { // Eat a piece of food and add it to the stomach
    console.log(`Yum! ${food} tasted great!`);
    this.stomach.push(food);
  }
  getState() { // Save the current state of the monster
    return new MonsterMemento(this.stomach.slice());
  }
  setState(memento) { // Restore the monster's previous state
    this.stomach = memento.getState();
    console.log(`Oops, I didn't like that ${this.stomach.pop()}...`);
  }
}

// Define the monster memento (memento)
class MonsterMemento {
  constructor(stomach) {
    this.stomach = stomach;
  }
  getState() {
    return this.stomach;
  }
}

// Define the monster caretaker (caretaker)
class MonsterCaretaker {
  constructor() {
    this.mementos = [];
  }
  addMemento(memento) { // Add a new memento to the list
    this.mementos.push(memento);
  }
  getMemento(index) { // Get the memento at the specified index
    return this.mementos[index];
  }
}

// Usage example
const monster = new HungryMonster();
const caretaker = new MonsterCaretaker();
monster.eat("Pizza");
caretaker.addMemento(monster.getState());
monster.eat("Taco");
caretaker.addMemento(monster.getState());
monster.eat("Sushi");
caretaker.addMemento(monster.getState());
monster.eat("Durian");
monster.setState(caretaker.getMemento(1));
console.log(`My stomach now contains: ${monster.stomach}`);

Класс HungryMonster является создателем, который ест разные виды пищи и сохраняет свое состояние как объект MonsterMemento. Метод getState используется для создания нового сувенира, который содержит текущее состояние монстра. Метод setState восстанавливает предыдущее состояние монстра, извлекая информацию о состоянии из сувенира.

Класс MonsterMemento — это сувенир, в котором хранится информация о состоянии монстра. У него есть метод getState, который возвращает сохраненное состояние.

Класс MonsterCaretaker — смотритель, который управляет сувенирами и позволяет монстру восстанавливать свое состояние. У него есть метод addMemento для добавления нового сувенира в список и метод getMemento для получения сувенира по указанному индексу.

3. Творческие шаблоны

Шаблоны создания — это набор шаблонов проектирования, которые предоставляют рецепт или план для создания объектов. Подобно тому, как шеф-повар готовит блюдо по рецепту, программист использует шаблон создания для создания объекта со всеми необходимыми параметрами и методами.

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

3–1. Фабрика

Вывод:шаблон Factory – это порождающий шаблон проектирования, предоставляющий интерфейс для создания объектов в суперклассе, но позволяющий подклассам изменять тип создаваемых объектов. Это полезно, когда вы хотите создать несколько объектов одного типа, не раскрывая логику создания клиенту. Шаблон Factory способствует слабой связи между классами и делает код более гибким и простым в обслуживании.

Как:

  • Определите интерфейс или абстрактный класс для создания объектов (фабрика)
  • Создайте конкретные подклассы, которые реализуют фабричный интерфейс
  • Клиентский код использует фабрику для создания объектов вместо их непосредственного создания.

Почему:

  • Инкапсулирует логику создания объектов в отдельные классы, упрощая поддержку и расширение кода.
  • Способствует слабой связи между классами, позволяя клиентам использовать фабричный интерфейс, а не определенные классы.
  • Позволяет легко заменять различные реализации одного и того же интерфейса, повышая гибкость и масштабируемость.

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

// Define an interface for creating cars
class CarFactory {
  createCar() {}
}

// Concrete subclasses that implement the factory interface for different types of car models
class ModelSFactory extends CarFactory {
  createCar() {
    return new ModelS();
  }
}

class Model3Factory extends CarFactory {
  createCar() {
    return new Model3();
  }
}

// Car classes created by the factory
class ModelS {
  get model() {
    return 'Model S';
  }
}

class Model3 {
  get model() {
    return 'Model 3';
  }
}

// Client code that uses the factory to create cars
function orderCar(factory) {
  const car = factory.createCar();
  console.log(`Building a ${car.model}...`);
}

// Order a Model S
const modelSFactory = new ModelSFactory();
orderCar(modelSFactory);

// Order a Model 3
const model3Factory = new Model3Factory();
orderCar(model3Factory);

В этом примере CarFactory определяет интерфейс, который создает автомобили. ModelSFactory и Model3Factory — это конкретные подклассы, которые реализуют заводской интерфейс для различных типов моделей автомобилей. Классы ModelS и Model3 — это классы автомобилей, созданные на заводе.

Клиентский код использует фабрику для создания автомобилей, вызывая метод createCar() для соответствующего экземпляра фабрики. Функция orderCar() принимает завод в качестве аргумента и регистрирует сообщение, указывающее модель строящегося автомобиля.

3–2. Строитель

Вывод:шаблон Builder — это творческий шаблон проектирования, который отделяет построение объекта от его представления. Это позволяет вам создавать сложные объекты шаг за шагом, используя класс построителя, который предоставляет методы для каждого шага процесса. Конечным результатом является гибкий и расширяемый способ создания объектов с различными конфигурациями, позволяющий избежать «ада конструкторов».

Как:

  • Определите интерфейс Builder, который определяет шаги, необходимые для создания объекта.
  • Создайте один или несколько конкретных конструкторов, которые реализуют интерфейс Builder, предоставляя методы для каждого шага процесса построения.
  • При необходимости определите класс Director, который инкапсулирует процесс построения и использует Builder для создания конечного объекта.

Почему:

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

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

class Post {
  constructor() {
    this.image = null;
    this.caption = null;
    this.location = null;
    this.tags = [];
  }
}

class PostBuilder {
  constructor() {
    this.post = new Post();
  }

  withImage(image) {
    this.post.image = image;
    return this;
  }

  withCaption(caption) {
    this.post.caption = caption;
    return this;
  }

  withLocation(location) {
    this.post.location = location;
    return this;
  }

  withTag(tag) {
    this.post.tags.push(tag);
    return this;
  }

  build() {
    return this.post;
  }
}

const post = new PostBuilder()
  .withImage('beautiful-sunset.jpg')
  .withCaption("I don't always post sunset pics, but when I do, they're spectacular!")
  .withLocation('Somewhere over the rainbow')
  .withTag('#sunset')
  .build();

console.log(post);

Сам код использует шаблон Builder для создания объекта Post с различными атрибутами, такими как изображение, заголовок и местоположение, а также с возможностью добавления тегов.

3–3. Синглтон

Обзор.Шаблон Singleton – это порождающий шаблон проектирования, который ограничивает создание экземпляра класса одним объектом. Это гарантирует наличие только одного экземпляра класса во всем приложении. Шаблон Singleton обычно используется при работе с ресурсоемкими объектами или при координации доступа к общим ресурсам.

Как:

  • Сделайте конструктор класса закрытым, чтобы предотвратить внешнее создание экземпляров.
  • Создайте статический метод, который разрешает доступ к одному экземпляру класса.
  • Лениво инициализировать экземпляр, если он еще не создан

Почему:

  • Ограничение создания экземпляра класса одним объектом может сэкономить системные ресурсы и повысить производительность.
  • Он обеспечивает глобальную точку доступа к общему ресурсу или сервису.
  • Шаблон Singleton также может упростить управление объектами с отслеживанием состояния, гарантируя наличие только одного экземпляра с одним источником достоверности.

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

class ProductLocationDatabase {
  constructor() {
    if (!ProductLocationDatabase.instance) {
      this._data = {};
      ProductLocationDatabase.instance = this;
    }
    return ProductLocationDatabase.instance;
  }

  add(product, location) {
    this._data[product] = location;
  }

  remove(product) {
    delete this._data[product];
  }

  getLocation(product) {
    return this._data[product];
  }
}

// Usage
const MiladDB1 = new ProductLocationDatabase();
const MiladDB2 = new ProductLocationDatabase();

MiladDB1.add('Toothpaste', 'A23');
MiladDB1.add('Shampoo', 'B87');

console.log(BobDB2.getLocation('Shampoo')); // B87

В этом примере мы создаем класс ProductLocationDatabase, используя шаблон Singleton. Когда Милад вызывает new ProductLocationDatabase(), создается только один экземпляр класса, который он может использовать для отслеживания местоположения каждого товара на своем складе. Всякий раз, когда он пытается создать новый экземпляр, вместо этого он возвращает существующий экземпляр.

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

3–4. Опытный образец

Вы можете пропустить это! (если вы не разработчик JavaScript)

Обзор. Шаблон прототипа – это шаблон проектирования, в котором объекты создаются путем клонирования существующих объектов. Это позволяет создавать новые объекты с минимальными затратами, поскольку они основаны на уже существующих прототипах. Этот шаблон обычно используется в JavaScript для создания похожих объектов без необходимости повторения кода.

Как:

  • Создайте объект, который служит прототипом.
  • Определите свойства и методы, которые должны быть общими для всех экземпляров.
  • Используйте клонирование объектов для создания новых экземпляров на основе прототипа.

Почему:

  • Позволяет эффективно создавать объекты с минимальными накладными расходами.
  • Снижает потребность в повторяющемся коде за счет совместного использования свойств и методов экземплярами.
  • Может использоваться в JavaScript для создания подобных объектов без необходимости определять каждый экземпляр отдельно.

Пример.Наш герой, JavaScript, пытается создать язык программирования с нуля. Он изо всех сил пытается придумать способ определения различных типов переменных.

// Define a base object
const variablePrototype = {
  type: "unknown",
  value: null,
  logValue() {
    console.log(`The value of this ${this.type} variable is ${this.value}`);
  }
};

// Clone the base object to create new variable types
const stringVariable = Object.create(variablePrototype);
stringVariable.type = "string";
stringVariable.value = "Hello, world!";

const numberVariable = Object.create(variablePrototype);
numberVariable.type = "number";
numberVariable.value = 42;

// Test the new variable types
stringVariable.logValue(); // Output: The value of this string variable is Hello, world!
numberVariable.logValue(); // Output: The value of this number variable is 42

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

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

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

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

Хотя примеры кода в этой статье написаны с использованием JavaScript, не стесняйтесь вносить свой вклад или исследовать этот репозиторий и для других языков программирования.

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

Эта статья была написана и усовершенствована с помощью GPT-3.5, мощной языковой модели, разработанной OpenAI.