"Банда четырёх" (Gang of Four или GoF) — Эрих Гамма, Ричард Хелм, Ральф Джонсон и Джон Влиссидес — в 1994 году сформировали канон шаблонов, который выдержал проверку временем. И хотя C++ претерпел немало изменений с тех пор (попробуйте сравнить C++98 и C++20 — это как сопоставить телегу с космическим кораблем), подходы GoF остаются актуальными.
Однако между теорией и практикой лежит пропасть размером с Большой каньон. Одно дело — понимать суть Абстрактной фабрики, и совсем другое — реализовать её эффективно, с учётом всех особенностей современного C++. Когда шаблон превращается в антипаттерн? Как применить лямбды в Стратегии? Что делать с потокобезопасностью в Одиночке?
Практическое руководство по шаблонам проектирования GoF на C++
Шаблоны проектирования — это не просто академическая теория для диссертаций и пыльных книжных полок. Это концентрат десятилетий коллективного опыта, собранный в компактные, многократно проверенные решения. В современной разработке, когда проекты превращаются в монстров о сотнях тысяч строк кода, паттерны становятся спасательным кругом для команд, тонущих в море технического долга. Хорошо реализованный паттерн экономит самый дорогой ресурс — время. Время на понимание кода, время на отладку, время на внедрение новых фич. Однажды я работал над рефакторингом системы обработки платежей, где абсолютно идентичный код был скопирован и немного изменен для каждого нового типа платежа (их было 17). После применения Стратегии и Фабрики кодовая база сократилась на 60%. А время на добавление нового типа платежа уменьшилось с недели до нескольких часов.
Но паттерны — это не волшебная палочка. Нельзя просто заявить: "А давайте применим Декоратор" — и ожидать мгновенных улучшений. Неудачно примененный паттерн может превратить проект в запутанный лабиринт абстракций. Как сказал Кент Бек: "Паттерны должны сидеть в голове, а не бросаться в глаза из кода."
В контексте C++ шаблоны проектирования имеют свои особенности. Строгая типизация, перегрузка операторов, шаблонное метапрограммирование, управление памятью — все это влияет на реализацию классических паттернов. То, что выглядит элегантно в Java, может потребовать хитрых трюков в C++. И наоборот, некоторые паттерны, например, CRTP (Curiously Recurring Template Pattern), особенно хорошо ложатся на почву шаблонного программирования в C++.
Ещё одна причина, почему GoF паттерны остаются актуальными — они помогают разговаривать на общем языке. Когда разработчик говорит "давай используем Посетителя для этой структуры", он мгновенно передаёт коллегам целый образ решения. Это как шахматная нотация — краткая запись вместо подробного объяснения каждого хода.
За годы практики я пришел к выводу, что понимание паттернов приходит через их воплощение в коде. Можно сколько угодно читать о Наблюдателе, но пока не реализуешь его сам и не увидишь, как взаимодействуют объекты, глубокого понимания не возникнет. Поэтому в этой статье мы фокусируемся на практических реализациях, на том как теоретические концепции превращаются в живой, работающий код. Пожалуй, главная ценность шаблонов проектирования — в том, что они помогают писать код для людей, а не только для компьютера. А учитывая, что программисты проводят гораздо больше времени читая код, чем пишя его, это качество переоценить невозможно.
Какие GOF-паттерны выбрать? Файл с задачками прикреплен (Экзамен DP.doc). К каждой задачке нужно подобрать паттерн, лучше всего... Объектно-ориентированного проектирования и проектирования на основе структур данных Помогите решить задание, так как вообще не понимаю, что тут можно сделать.
Решить задание с... Знание шаблонов проектирования. Что знать ? В вакансиях постояно пишут: знание шаблонов проектирования. Как я понимаю, ш.п. это уже... Реализация шаблонов класса в инлайн файле Пытался написать шаблонны
MyClass.h
#pragma once
#define MYCLASS
template <typename T>...
Что такое шаблоны проектирования GoF и почему они актуальны сегодня
Когда "Банда четырёх" выпустила свою книгу "Design Patterns: Elements of Reusable Object-Oriented Software", многие воспринали её как откровение. Впервые разрозненные практики и подходы были систематизированы, каталогизированы и представлены миру в виде стройной системы. Это был 1994 год — эпоха, когда ООП только набирало обороты, а C++ вытеснял C в качестве языка для серьёзного промышленного программирования.
Суть шаблонов проектирования — предложить типовое решение для часто встречающейся проблемы. Вы же не изобретаете велосипед каждый раз, когда нужно добраться из пункта А в пункт Б? Точно так же нет смысла заново придумывать способы создания объектов, структурирования компонентов или организации взаимодействия между ними.
Все 23 паттерна GoF делятся на три категории:- Порождающие (Creational) — отвечают за механизмы создания экземпляров классов.
- Структурные (Structural) — опредиляют отношения между объектами и классами.
- Поведенческие (Behavioral) — определяют взаимодействие объектов между собой.
Шаблоны GoF удивительным образом перекликаются с принципами SOLID. Взять хотя бы принцип единственной ответственности (Single Responsibility Principle) — он прослеживается в большинстве паттернов. А принцип открытости/закрытости (Open/Closed Principle) лежит в основе таких паттернов как Стратегия, Декоратор и Состояние. Принцип подстановки Лисков (Liskov Substitution Principle)? Посмотрите на Шаблонный метод. Принцип инверсии зависимостей (Dependency Inversion Principle)? Это вся суть Абстрактной фабрики и Фабричного метода.
Конечно, не обходится и без критики. Самое распространённое обвинение — избыточная сложность. И я чостично согласен с этим. Видел проекты, где разработчики с горящими глазами применяли паттерны просто чтобы они были. В результате — адская смесь из фабрик для создания декораторов, оборачивающих адаптеры, внутри которых скрывались стратегии... Вспоминаю это и вздрагиваю. Другой аргумент против — некоторые паттерны встроены в современные языки. Например, Итератор в C++ стал частью стандартной библиотеки. Зачем изобретать велосипед, если в STL уже есть std::iterator ?
Но несмотря на критику, шаблоны GoF остаются актуальными. Почему?
Во-первых, они независимы от языка. Хотя книга GoF содержит примеры на C++ и Smalltalk, концепции применимы в любом объектно-ориентированном языке — от Java до Python.
Во-вторых, они проверены временем. За почти 30 лет существования паттернов не появилось ничего принципиально нового. Даже "модные" сейчас функциональные паттерны вроде монад, функторов и аппликативных функторов можно рассматривать как расширение идеи GoF.
В-третьих, шаблоны GoF — это своеобразные кирпичики для построения больших архитектурных решений. Такие архитектурные подходы как MVC, MVVM, Clean Architecture состоят из множества низкоуровневых паттернов.
Что касается C++, то здесь паттерны GoF приобретают особое значение. Мощный язык с множеством возможностей даёт большую свободу, а значит — и простор для ошибок. Паттерны помогают структурировать мышление и не заблудиться в дебрях указателей, ссылок, шаблонов и многопоточности. Реализация паттернов в современном C++ отличается от того, что было 30 лет назад. Умные указатели вместо сырых, стандартные контейнеры вместо самописных, лямбды вместо функциональных объектов... Но суть остаётся той же — решать типовые проблемы проверенными способами.
Порождающие шаблоны в C++
Порождающие паттерны — первый кит, на котором держится архитектура надёжного и гибкого кода. Это то, с чего начинается любая система: как создавать объекты? Казалось бы, что может быть проще оператора new ? Но когда ваш проект разрастается до десятков классов с сложными зависимостями, простое создание объектов превращается в отдельную проблему.
Фабричный метод (Factory Method)
Пожалуй, из всех порождающих паттернов Фабричный метод используется чаще всего. Его суть проста: делегировать создание объектов методу, который может быть переопределен в подклассах. Представьте, что вы разрабатываете игровой движок, где есть разные типы врагов. Каждый новый уровень может иметь своих уникальных врагов.
C++ | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
| // Продукт — базовый интерфейс
class Enemy {
public:
virtual void attack() = 0;
virtual ~Enemy() = default;
};
// Конкретные продукты
class Zombie : public Enemy {
public:
void attack() override {
std::cout << "Zombie bites you!" << std::endl;
}
};
class Vampire : public Enemy {
public:
void attack() override {
std::cout << "Vampire drains your blood!" << std::endl;
}
};
// Creator — базовый класс с фабричным методом
class LevelEnemyFactory {
public:
// Это и есть наш фабричный метод
virtual Enemy* createEnemy() = 0;
// Обратите внимание, как фабричный метод используется в алгоритме
void spawnEnemy() {
Enemy* enemy = createEnemy();
std::cout << "A new enemy appeared: ";
enemy->attack();
// В реальном коде здесь был бы более сложный алгоритм
delete enemy; // Don't forget to clean up!
}
virtual ~LevelEnemyFactory() = default;
};
// Конкретные создатели
class ZombieLevel : public LevelEnemyFactory {
public:
Enemy* createEnemy() override {
return new Zombie();
}
};
class VampireLevel : public LevelEnemyFactory {
public:
Enemy* createEnemy() override {
return new Vampire();
}
}; |
|
У этого паттерна есть важный нюанс — он не просто инкапсулирует логику создания, он позволяет подклассам выбирать тип создаваемого объекта. Это идеальное воплощение принципа открытости/закрытости: код открыт для расширения (можно добавить новые типы врагов и уровней), но закрыт для изменения (базовые абстракции остаются неизмеными).
Фабричный метод часто путают с "Простой фабрикой" (Simple Factory), которая по сути является обычным статическим методом для создания объектов. Простая фабрика — не полноценный паттерн, а просто приём программирования.
Но использование сырых указателей в 2023 году? Серьёзно? Давайте перепишем с использованием умных указателей:
C++ | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| class LevelEnemyFactory {
public:
virtual std::unique_ptr<Enemy> createEnemy() = 0;
void spawnEnemy() {
auto enemy = createEnemy();
std::cout << "A new enemy appeared: ";
enemy->attack();
// No need to delete explicitly, unique_ptr handles it
}
virtual ~LevelEnemyFactory() = default;
};
class ZombieLevel : public LevelEnemyFactory {
public:
std::unique_ptr<Enemy> createEnemy() override {
return std::make_unique<Zombie>();
}
}; |
|
Вот теперь лучше. Управление ресурсами стало автоматическим благодаря RAII.
Абстрактная фабрика (Abstract Factory)
А что если нам нужны не просто отдельные объекты, а целые семейства взаимосвязанных объектов? Тут на помощь приходит Абстрактная фабрика.
Допустим, в нашей игре есть разные "стили" для элементов интерфейса: средневековый и футуристический. Иконки, кнопки, меню — всё должно выдерживать единый стиль.
C++ | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
| // Абстрактные продукты
class Button {
public:
virtual void render() = 0;
virtual ~Button() = default;
};
class Menu {
public:
virtual void display() = 0;
virtual ~Menu() = default;
};
// Средневековые продукты
class MedievalButton : public Button {
public:
void render() override {
std::cout << "Rendering medieval stone button" << std::endl;
}
};
class MedievalMenu : public Menu {
public:
void display() override {
std::cout << "Showing parchment menu" << std::endl;
}
};
// Футуристические продукты
class FuturisticButton : public Button {
public:
void render() override {
std::cout << "Rendering holographic button" << std::endl;
}
};
class FuturisticMenu : public Menu {
public:
void display() override {
std::cout << "Showing floating neon menu" << std::endl;
}
};
// Абстрактная фабрика
class UIFactory {
public:
virtual std::unique_ptr<Button> createButton() = 0;
virtual std::unique_ptr<Menu> createMenu() = 0;
virtual ~UIFactory() = default;
};
// Конкретные фабрики
class MedievalUIFactory : public UIFactory {
public:
std::unique_ptr<Button> createButton() override {
return std::make_unique<MedievalButton>();
}
std::unique_ptr<Menu> createMenu() override {
return std::make_unique<MedievalMenu>();
}
};
class FuturisticUIFactory : public UIFactory {
public:
std::unique_ptr<Button> createButton() override {
return std::make_unique<FuturisticButton>();
}
std::unique_ptr<Menu> createMenu() override {
return std::make_unique<FuturisticMenu>();
}
}; |
|
Абстрактную фабрику часто используют вместе с фабричным методом. Методы абстрактной фабрики являются фабричными методами, но фокус здесь смещается с наследования на композицию. В более сложных случаях может понадобится целый кластер фабрик, создающих сложные объектные экосистемы. И тут становится очевиден минус данного паттерна — если нужно добавить новый продукт (скажем, ползунок для громкости), придётся изменять интерфейс UIFactory и все его реализации. Это нарушает принцип открытости/закрытости.
Строитель (Builder)
Иногда объекты настолько сложны, что их создание в одну строчку выглядит как акт насилия над кодом. Для таких случаев есть паттерн Строитель. Допустим, нам нужно создать сложный HTTP-запрос:
C++ | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
| class HttpRequest {
std::string method;
std::string url;
std::map<std::string, std::string> headers;
std::string body;
bool followRedirects;
int timeout;
public:
// Представьте себе конструктор с десятком параметров. Ужасно, правда?
HttpRequest(const std::string& method, const std::string& url,
const std::map<std::string, std::string>& headers,
const std::string& body, bool followRedirects, int timeout)
: method(method), url(url), headers(headers),
body(body), followRedirects(followRedirects), timeout(timeout) {}
// Геттеры и прочие методы...
};
// А вот с Строителем всё становится элегантно:
class HttpRequestBuilder {
private:
std::string method = "GET";
std::string url;
std::map<std::string, std::string> headers;
std::string body;
bool followRedirects = true;
int timeout = 30;
public:
HttpRequestBuilder& setMethod(const std::string& method) {
this->method = method;
return *this;
}
HttpRequestBuilder& setUrl(const std::string& url) {
this->url = url;
return *this;
}
HttpRequestBuilder& addHeader(const std::string& name, const std::string& value) {
headers[name] = value;
return *this;
}
HttpRequestBuilder& setBody(const std::string& body) {
this->body = body;
return *this;
}
HttpRequestBuilder& shouldFollowRedirects(bool follow) {
this->followRedirects = follow;
return *this;
}
HttpRequestBuilder& setTimeout(int seconds) {
this->timeout = seconds;
return *this;
}
std::unique_ptr<HttpRequest> build() {
if (url.empty()) {
throw std::logic_error("URL is required");
}
return std::make_unique<HttpRequest>(
method, url, headers, body, followRedirects, timeout
);
}
}; |
|
Теперь создание запроса выглядит так:
C++ | 1
2
3
4
5
6
7
| auto request = HttpRequestBuilder()
.setMethod("POST")
.setUrl("https://api.example.com/data")
.addHeader("Content-Type", "application/json")
.setBody("{\"key\": \"value\"}")
.setTimeout(60)
.build(); |
|
Эту технику часто называют "текучим (fluent) интерфейсом", и она сильно повышает читабельность кода. Обратите внимание на то, как каждый метод возвращает ссылку на текущий объект (*this ), что позволяет цеплять вызовы.
В классических реализациях Строителя также присутствует Директор — класс, определяющий последовательность вызовов. Это актуально, когда различные конфигурации объекта используются часто:
C++ | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| class HttpRequestDirector {
public:
std::unique_ptr<HttpRequest> createDefaultGetRequest(const std::string& url) {
return HttpRequestBuilder()
.setMethod("GET")
.setUrl(url)
.build();
}
std::unique_ptr<HttpRequest> createJsonPostRequest(
const std::string& url, const std::string& jsonBody) {
return HttpRequestBuilder()
.setMethod("POST")
.setUrl(url)
.addHeader("Content-Type", "application/json")
.setBody(jsonBody)
.build();
}
}; |
|
Прототип (Prototype)
Иногда создание объекта — затратный процесс, особенно если требуется инициализация из внешних ресурсов. В таких случаях проще клонировать существующий объект, чем создавать новый с нуля. Именно эту идею и воплощает паттерн Прототип.
C++ | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
| class Monster {
public:
virtual ~Monster() = default;
virtual std::unique_ptr<Monster> clone() const = 0;
virtual void display() const = 0;
};
class Dragon : public Monster {
private:
std::string name;
int fireBreathDamage;
public:
Dragon(const std::string& name, int damage)
: name(name), fireBreathDamage(damage) {}
std::unique_ptr<Monster> clone() const override {
// Возвращаем копию себя
return std::make_unique<Dragon>(*this);
}
void display() const override {
std::cout << "Dragon " << name << " with "
<< fireBreathDamage << " fire damage" << std::endl;
}
};
class Troll : public Monster {
private:
std::string clubType;
int clubDamage;
public:
Troll(const std::string& clubType, int damage)
: clubType(clubType), clubDamage(damage) {}
std::unique_ptr<Monster> clone() const override {
return std::make_unique<Troll>(*this);
}
void display() const override {
std::cout << "Troll with " << clubType
<< " dealing " << clubDamage << " damage" << std::endl;
}
}; |
|
Реальная сила Прототипа проявляется, когда мы используем реестр прототипов для создания новых объектов:
C++ | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| class MonsterPrototypeRegistry {
private:
std::unordered_map<std::string, std::unique_ptr<Monster>> prototypes;
public:
void registerPrototype(const std::string& name, std::unique_ptr<Monster> prototype) {
prototypes[name] = std::move(prototype);
}
std::unique_ptr<Monster> createMonster(const std::string& name) {
if (prototypes.find(name) == prototypes.end()) {
throw std::runtime_error("No such prototype: " + name);
}
return prototypes[name]->clone();
}
}; |
|
Одиночка (Singleton)
Паттерн Одиночка гарантирует, что у класса есть только один экземпляр, и предоставляет глобальную точку доступа к нему. Классика жанра выглядит так:
C++ | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
| class GameSettings {
private:
// Статическая переменная для хранения единственного экземпляра
static GameSettings* instance;
std::string difficultyLevel;
bool soundEnabled;
float volume;
// Приватный конструктор, чтобы никто не мог создать новый экземпляр
GameSettings() : difficultyLevel("medium"), soundEnabled(true), volume(0.7f) {}
// Запрещаем копирование и присваивание
GameSettings(const GameSettings&) = delete;
GameSettings& operator=(const GameSettings&) = delete;
public:
// Метод для получения экземпляра
static GameSettings& getInstance() {
// "Ленивая" инициализация — создаём объект только при первом вызове
if (instance == nullptr) {
instance = new GameSettings();
}
return *instance;
}
// Методы для работы с настройками
void setDifficulty(const std::string& level) {
difficultyLevel = level;
}
std::string getDifficulty() const {
return difficultyLevel;
}
// И другие методы...
};
// Инициализируем статическую переменную
GameSettings* GameSettings::instance = nullptr; |
|
Но такая реализация не потокобезопасна! Если два потока одновременно вызовут getInstance() и увидят, что instance == nullptr , они оба создадут экземпляр. К счастью, С++11 предоставил нам простое решение:
C++ | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| class GameSettings {
private:
std::string difficultyLevel;
bool soundEnabled;
float volume;
GameSettings() : difficultyLevel("medium"), soundEnabled(true), volume(0.7f) {}
public:
// Обратите внимание на реализацию
static GameSettings& getInstance() {
// Объявляем статическую переменную внутри метода
// Гарантируется, что она будет инициализирована только один раз,
// даже в многопоточной среде
static GameSettings instance;
return instance;
}
// Методы для работы с настройками...
}; |
|
Структурные шаблоны в C++
После освоения порождающих шаблонов закономерно возникает вопрос: "Окей, я научился создавать объекты, но как их организовать в стройную систему?" Здесь на сцену выходят структурные паттерны — они определяют, как объекты и классы могут компоноваться для формирования более сложных структур.
Адаптер (Adapter)
Представьте, что вы купили крутой японский гаджет, а дома обнаружили, что вилка не подходит к российской розетке. Что делать? Использовать переходник-адаптер. В мире кода происходит то же самое: иногда интерфейс одного класса не соответствует тому, что ожидает система.
C++ | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
| // Устаревший интерфейс, который мы не можем изменить
class LegacyRectangle {
public:
void oldDraw(int x, int y, int width, int height) {
std::cout << "Drawing rectangle at (" << x << "," << y
<< ") with width " << width << " and height " << height << std::endl;
}
};
// Новый интерфейс, который ожидает наша система
class Shape {
public:
virtual void draw(int x, int y, int size) = 0;
virtual ~Shape() = default;
};
// Адаптер, который позволяет использовать LegacyRectangle как Shape
class RectangleAdapter : public Shape {
private:
std::unique_ptr<LegacyRectangle> adaptee;
public:
RectangleAdapter() : adaptee(std::make_unique<LegacyRectangle>()) {}
void draw(int x, int y, int size) override {
// Предположим, что size определяет и ширину, и высоту
adaptee->oldDraw(x, y, size, size);
}
}; |
|
В реальных проектах я часто встречал адаптеры, оборачивающие библиотеки третьих сторон. Это позволяет легко заменить одну библиотеку на другую, если понадобится — главное, чтобы адаптер реализовывал нужный вам интерфейс.
Существуют две разновидности адаптера: через наследование (как в примере выше) и через композицию. Второй вариант гибче, так как позволяет адаптировать несколько разных классов к одному интерфейсу.
Мост (Bridge)
Иногда абстракция и реализация настолько связаны, что любое изменение одной стороны требует корректировки другой. Паттерн Мост разрывает эту связь, позволяя абстракции и реализации эволюционировать независимо.
C++ | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
| // Реализация
class RenderingEngine {
public:
virtual void renderCircle(float x, float y, float radius) = 0;
virtual ~RenderingEngine() = default;
};
// Конкретные реализации
class OpenGLRenderer : public RenderingEngine {
public:
void renderCircle(float x, float y, float radius) override {
std::cout << "Rendering circle using OpenGL at (" << x << "," << y
<< ") with radius " << radius << std::endl;
}
};
class DirectXRenderer : public RenderingEngine {
public:
void renderCircle(float x, float y, float radius) override {
std::cout << "Rendering circle using DirectX at (" << x << "," << y
<< ") with radius " << radius << std::endl;
}
};
// Абстракция
class Shape {
protected:
RenderingEngine* renderer;
public:
Shape(RenderingEngine* renderer) : renderer(renderer) {}
virtual void draw() = 0;
virtual ~Shape() = default;
};
// Уточнённая абстракция
class Circle : public Shape {
private:
float x, y, radius;
public:
Circle(float x, float y, float radius, RenderingEngine* renderer)
: Shape(renderer), x(x), y(y), radius(radius) {}
void draw() override {
renderer->renderCircle(x, y, radius);
}
}; |
|
Ключевое преимущество моста — разделение двух ортогональных измерений. Например, в графическом приложении мы можем иметь несколько видов фигур (круг, квадрат) и несколько рендереров (OpenGL, DirectX). Без Моста нам бы потребовалось n*m классов (OpenGLCircle, DirectXCircle, OpenGLSquare...). С Мостом достаточно n+m классов.
Компоновщик (Composite)
Представьте дерево каталогов файловой системы: папка может содержать как файлы, так и вложенные папки. Как единообразно работать со всеми этими объектами? Паттерн Компоновщик объединяет объекты в древовидные структуры для представления иерархий часть-целое.
C++ | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
| // Компонент — общий интерфейс для листьев и составных объектов
class FileSystemNode {
protected:
std::string name;
public:
FileSystemNode(const std::string& name) : name(name) {}
virtual void ls(int depth = 0) const = 0;
virtual size_t getSize() const = 0;
virtual ~FileSystemNode() = default;
};
// Лист
class File : public FileSystemNode {
private:
size_t size;
public:
File(const std::string& name, size_t size)
: FileSystemNode(name), size(size) {}
void ls(int depth = 0) const override {
std::cout << std::string(depth, ' ') << name << " (" << size << " bytes)" << std::endl;
}
size_t getSize() const override {
return size;
}
};
// Составной объект
class Directory : public FileSystemNode {
private:
std::vector<std::unique_ptr<FileSystemNode>> children;
public:
Directory(const std::string& name) : FileSystemNode(name) {}
void add(std::unique_ptr<FileSystemNode> child) {
children.push_back(std::move(child));
}
void ls(int depth = 0) const override {
std::cout << std::string(depth, ' ') << name << "/" << std::endl;
for (const auto& child : children) {
child->ls(depth + 2);
}
}
size_t getSize() const override {
size_t totalSize = 0;
for (const auto& child : children) {
totalSize += child->getSize();
}
return totalSize;
}
}; |
|
Компоновщик отлично подходит для сложных иерархических структур: GUI компоненты, абстрактный синтаксический деревья, организационные структуры. Один из минусов — отсутствие типобезопасности, так как общий интерфейс должен включать методы как для листьев, так и для составных объектов.
Декоратор (Decorator)
Когда-нибудь заказывали кофе в модной кофейне? "Латте с соевым молоком, карамельным сиропом и корицей". По сути, вы декорируете базовый продукт (кофе) дополнительными свойствами. Паттерн Декоратор позволяет добавлять объекту новые обязанности динамически, без создания подклассов.
C++ | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
| // Компонент — интерфейс для всех декораторов и основного объекта
class Coffee {
public:
virtual double getCost() const = 0;
virtual std::string getDescription() const = 0;
virtual ~Coffee() = default;
};
// Конкретный компонент
class SimpleCoffee : public Coffee {
public:
double getCost() const override {
return 1.0;
}
std::string getDescription() const override {
return "Simple coffee";
}
};
// Базовый декоратор
class CoffeeDecorator : public Coffee {
protected:
std::unique_ptr<Coffee> decoratedCoffee;
public:
CoffeeDecorator(std::unique_ptr<Coffee> coffee)
: decoratedCoffee(std::move(coffee)) {}
double getCost() const override {
return decoratedCoffee->getCost();
}
std::string getDescription() const override {
return decoratedCoffee->getDescription();
}
};
// Конкретные декораторы
class MilkDecorator : public CoffeeDecorator {
public:
using CoffeeDecorator::CoffeeDecorator;
double getCost() const override {
return decoratedCoffee->getCost() + 0.5;
}
std::string getDescription() const override {
return decoratedCoffee->getDescription() + ", with milk";
}
};
class CaramelDecorator : public CoffeeDecorator {
public:
using CoffeeDecorator::CoffeeDecorator;
double getCost() const override {
return decoratedCoffee->getCost() + 0.6;
}
std::string getDescription() const override {
return decoratedCoffee->getDescription() + ", with caramel";
}
}; |
|
Использование паттерна чрезвычайно гибкое:
C++ | 1
2
3
4
5
6
7
8
9
10
| auto myCoffee = std::make_unique<SimpleCoffee>();
std::cout << myCoffee->getDescription() << ": $" << myCoffee->getCost() << std::endl;
auto milkCoffee = std::make_unique<MilkDecorator>(std::move(myCoffee));
std::cout << milkCoffee->getDescription() << ": $" << milkCoffee->getCost() << std::endl;
auto fancyCoffee = std::make_unique<CaramelDecorator>(
std::make_unique<MilkDecorator>(
std::make_unique<SimpleCoffee>()));
std::cout << fancyCoffee->getDescription() << ": $" << fancyCoffee->getCost() << std::endl; |
|
Декоратор широко используется в библиотеках ввода-вывода (например, Java IO). В C++ стандартная библиотека потоков тоже использует этот принцип: вы можете декорировать базовые потоки с помощью манипуляторов, буферизации и т.д.
Фасад (Facade)
Фасад — один из самых простых и одновременно полезных паттернов. Он предоставляет унифицированный интерфейс к набору интерфейсов в подсистеме, скрывая её сложность. Вспомните пульт для TV. Мы нажимаем одну кнопку "включить", а внутри происходит сложный процесс: инициализация дисплея, загрузка настроек, калибровка звука и т.д. Фасад работает так же — скрывает сложные детали за простым интерфейсом.
C++ | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
| // Пример сложной подсистемы
class CPU {
public:
void freeze() { std::cout << "CPU: Freezing..." << std::endl; }
void jump(long position) { std::cout << "CPU: Jumping to position " << position << std::endl; }
void execute() { std::cout << "CPU: Executing instructions..." << std::endl; }
};
class Memory {
public:
void load(long position, const std::string& data) {
std::cout << "Memory: Loading data at position " << position << std::endl;
}
};
class HardDrive {
public:
std::string read(long lba, int size) {
std::cout << "HardDrive: Reading data from sector " << lba << std::endl;
return "data";
}
};
// Фасад, скрывающий сложность подсистемы
class ComputerFacade {
private:
CPU cpu;
Memory memory;
HardDrive hardDrive;
public:
void start() {
cpu.freeze();
memory.load(0, hardDrive.read(0, 1024));
cpu.jump(0);
cpu.execute();
}
}; |
|
Использование фасада невероятно просто:
C++ | 1
2
| ComputerFacade computer;
computer.start(); // Запуск компьютера одной командой |
|
Фасад ничего не знает о внутренней логике подсистем — он просто соединяет их вместе, предоставляя клиенту упрощенный интерфейс. Он особенно полезен когда вам нужно взаимодействовать со сложной, запутаной системой, но вам нужен только ограниченный набор функциональности.
Приспособленец (Flyweight)
Когда вы разрабатываете игру с тысячами почти идентичных объектов (деревья в лесу, солдаты на поле боя), память может быстро стать дефицитным ресурсом. Паттерн Приспособленец решает эту проблему, разделяя объект на две части: внутреннее неизменяемое состояние (intrinsic), которое можно разделить между объектами, и внешнее изменяемое состояние (extrinsic), которое уникально для каждого объекта.
C++ | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
| // Приспособленец — текстура для частиц
class ParticleTexture {
private:
std::string texturePath;
std::vector<uint8_t> textureData; // Потенциально большой объем данных
public:
ParticleTexture(const std::string& path) : texturePath(path) {
// Представим, что здесь происходит загрузка текстуры
std::cout << "Loading texture: " << path << std::endl;
textureData.resize(1024 * 1024, 1); // 1MB "текстура"
}
void render(float x, float y, float scale, float rotation) const {
std::cout << "Rendering texture at (" << x << "," << y
<< ") with scale " << scale << " and rotation " << rotation << std::endl;
}
};
// Фабрика приспособленцев
class ParticleTextureFactory {
private:
std::unordered_map<std::string, std::shared_ptr<ParticleTexture>> textures;
public:
std::shared_ptr<ParticleTexture> getTexture(const std::string& path) {
auto it = textures.find(path);
if (it == textures.end()) {
textures[path] = std::make_shared<ParticleTexture>(path);
}
return textures[path];
}
};
// Клиентский код: частица, использующая приспособленца
class Particle {
private:
std::shared_ptr<ParticleTexture> texture; // Внутреннее, разделяемое состояние
float x, y, scale, rotation; // Внешнее, уникальное состояние
public:
Particle(std::shared_ptr<ParticleTexture> tex, float x, float y)
: texture(tex), x(x), y(y), scale(1.0f), rotation(0.0f) {}
void update(float dt) {
// Обновление внешнего состояния
rotation += dt * 45.0f; // 45 градусов в секунду
}
void render() const {
texture->render(x, y, scale, rotation);
}
}; |
|
Приспособленец позволяет иметь тысячи частиц, которые используют одну и ту же текстуру без дублирования данных. В реальных проектах этот паттерн можно встретить в игровых движках, текстовых редакторах (где символы — приспособленцы) и системах рендеринга.
Заместитель (Proxy)
Иногда нам нужно контролировать доступ к объекту: отложить его создание до реального использования, проверить права доступа или кешировать результаты. Паттерн Заместитель обеспечивает суррогат для другого объекта, контролируя доступ к нему.
C++ | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
| // Интерфейс для реального объекта и заместителя
class Image {
public:
virtual void display() = 0;
virtual ~Image() = default;
};
// Реальный объект, который загружает изображение
class RealImage : public Image {
private:
std::string filename;
void loadFromDisk() {
std::cout << "Loading image from disk: " << filename << std::endl;
// Тяжелая операция загрузки изображения
}
public:
RealImage(const std::string& filename) : filename(filename) {
loadFromDisk();
}
void display() override {
std::cout << "Displaying image: " << filename << std::endl;
}
};
// Заместитель, который отложит загрузку до момента вызова display()
class ProxyImage : public Image {
private:
std::string filename;
std::unique_ptr<RealImage> realImage;
public:
ProxyImage(const std::string& filename) : filename(filename) {}
void display() override {
if (!realImage) {
realImage = std::make_unique<RealImage>(filename);
}
realImage->display();
}
}; |
|
Существуют различные типы заместителей:- Виртуальный заместитель (как в примере выше) — откладывает создание тяжелого объекта.
- Защищающий заместитель — проверяет права доступа.
- Кэширующий заместитель — сохраняет результаты запросов.
- Умная ссылка — добавляет дополнительные действия при обращении к объекту (например, подсчет ссылок).
Заместитель особенно полезен в условиях ограниченных ресурсов или при работе с удаленными объектами (RPC/RMI).
Поведенческие шаблоны в C++
Окей, мы научились создавать объекты с помощью порождающих паттернов и структурировать их с помощью структурных. Но как заставить их красиво общаться между собой? Тут в игру вступают поведенческие шаблоны — самая многочисленная и, пожалуй, самая интересная категория.
Цепочка обязанностей (Chain of Responsibility)
Часто в проектах возникает ситуация, когда запрос должен быть обработан одним из нескольких объектов, но заранее неизвестно, каким именно. Паттерн "Цепочка обязанностей" позволяет передавать запрос по цепочке потенциальных обработчиков, пока один из них не обработает его.
Представьте систему логирования, где сообщения разных уровней обрабатываются разными обработчиками:
C++ | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
| enum class LogLevel {
INFO,
WARNING,
ERROR
};
// Базовый обработчик
class Logger {
protected:
std::unique_ptr<Logger> nextLogger;
LogLevel level;
public:
Logger(LogLevel level) : level(level) {}
void setNext(std::unique_ptr<Logger> next) {
nextLogger = std::move(next);
}
virtual void log(LogLevel messageLevel, const std::string& message) {
if (this->level <= messageLevel) {
writeMessage(message);
}
if (nextLogger) {
nextLogger->log(messageLevel, message);
}
}
virtual void writeMessage(const std::string& message) = 0;
virtual ~Logger() = default;
};
// Конкретные обработчики
class ConsoleLogger : public Logger {
public:
ConsoleLogger(LogLevel level) : Logger(level) {}
void writeMessage(const std::string& message) override {
std::cout << "Console Logger: " << message << std::endl;
}
};
class FileLogger : public Logger {
public:
FileLogger(LogLevel level) : Logger(level) {}
void writeMessage(const std::string& message) override {
std::cout << "File Logger: " << message << std::endl;
// В реальности здесь был бы код для записи в файл
}
};
class EmailLogger : public Logger {
public:
EmailLogger(LogLevel level) : Logger(level) {}
void writeMessage(const std::string& message) override {
std::cout << "Email Logger: " << message << std::endl;
// Отправка email в реальном коде
}
}; |
|
Построение цепочки и использование выглядит так:
C++ | 1
2
3
4
5
6
7
8
9
10
11
12
| // Строим цепочку обработчиков
auto consoleLogger = std::make_unique<ConsoleLogger>(LogLevel::INFO);
auto fileLogger = std::make_unique<FileLogger>(LogLevel::WARNING);
auto emailLogger = std::make_unique<EmailLogger>(LogLevel::ERROR);
fileLogger->setNext(std::move(emailLogger));
consoleLogger->setNext(std::move(fileLogger));
// Используем
consoleLogger->log(LogLevel::INFO, "This is an information");
consoleLogger->log(LogLevel::WARNING, "This is a warning");
consoleLogger->log(LogLevel::ERROR, "This is an error"); |
|
Этот паттерн часто используется в обработчиках событий UI, фильтрах запросов, системах обработки исключений.
Команда (Command)
Паттерн "Команда" — один из моих любимых. Он превращает запрос в объект, позволяя параметризовать клиентов разными запросами, ставить запросы в очередь, логировать их и поддерживать отмену операций. Представьте кнопки в текстовом редакторе:
C++ | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
| // Интерфейс команды
class Command {
public:
virtual void execute() = 0;
virtual void undo() = 0;
virtual ~Command() = default;
};
// Получатель команды
class TextEditor {
private:
std::string text;
public:
void insertText(const std::string& newText) {
text += newText;
std::cout << "Text after insertion: " << text << std::endl;
}
void deleteText(int numChars) {
if (numChars <= text.length()) {
text.resize(text.length() - numChars);
std::cout << "Text after deletion: " << text << std::endl;
}
}
std::string getText() const {
return text;
}
};
// Конкретные команды
class InsertTextCommand : public Command {
private:
TextEditor& editor;
std::string textToInsert;
public:
InsertTextCommand(TextEditor& editor, const std::string& text)
: editor(editor), textToInsert(text) {}
void execute() override {
editor.insertText(textToInsert);
}
void undo() override {
editor.deleteText(textToInsert.length());
}
};
class DeleteTextCommand : public Command {
private:
TextEditor& editor;
int numChars;
std::string deletedText;
public:
DeleteTextCommand(TextEditor& editor, int chars)
: editor(editor), numChars(chars) {}
void execute() override {
std::string currentText = editor.getText();
if (numChars <= currentText.length()) {
deletedText = currentText.substr(currentText.length() - numChars);
editor.deleteText(numChars);
}
}
void undo() override {
editor.insertText(deletedText);
}
};
// Инвокер — объект, вызывающий команды
class CommandInvoker {
private:
std::vector<std::unique_ptr<Command>> executedCommands;
public:
void executeCommand(std::unique_ptr<Command> command) {
command->execute();
executedCommands.push_back(std::move(command));
}
void undoLastCommand() {
if (!executedCommands.empty()) {
executedCommands.back()->undo();
executedCommands.pop_back();
}
}
}; |
|
Использование:
C++ | 1
2
3
4
5
6
7
8
9
10
11
12
| TextEditor editor;
CommandInvoker invoker;
// Вставляем текст
invoker.executeCommand(std::make_unique<InsertTextCommand>(editor, "Hello, "));
invoker.executeCommand(std::make_unique<InsertTextCommand>(editor, "World!"));
// Удаляем последние 6 символов
invoker.executeCommand(std::make_unique<DeleteTextCommand>(editor, 6));
// Отменяем последнюю команду (возвращаем "World!")
invoker.undoLastCommand(); |
|
Ключевое преимущество паттерна "Команда" — это разделение объекта, знающего как выполнить действие (получатель), от объекта, который запрашивает выполнение (инвокер). Это позволяет реализовать функциональность отмены/повтора, очереди команд, макросы команд и другие продвинутые возможности.
Итератор (Iterator)
Паттерн "Итератор" предоставляет способ последовательного доступа к элементам агрегата без раскрытия его внутренней структуры. В C++ этот паттерн настолько фундаментален, что встроен в стандартную библиотеку через концепцию итераторов и алгоритмов. Но давайте реализуем свой собственный итератор для лучшего понимания:
C++ | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
| // Агрегат — коллекция целых чисел с шагом
class NumberSequence {
private:
int start;
int step;
int count;
public:
NumberSequence(int start, int step, int count)
: start(start), step(step), count(count) {}
// Внутренний класс итератора
class Iterator {
private:
const NumberSequence& sequence;
int currentIndex;
public:
Iterator(const NumberSequence& seq, int index)
: sequence(seq), currentIndex(index) {}
bool operator!=(const Iterator& other) const {
return currentIndex != other.currentIndex;
}
Iterator& operator++() {
currentIndex++;
return *this;
}
int operator*() const {
return sequence.start + sequence.step * currentIndex;
}
};
Iterator begin() const {
return Iterator(*this, 0);
}
Iterator end() const {
return Iterator(*this, count);
}
}; |
|
Использование:
C++ | 1
2
3
4
5
| NumberSequence evenNumbers(2, 2, 5); // 2, 4, 6, 8, 10
for (int num : evenNumbers) {
std::cout << num << " "; // Выведет: 2 4 6 8 10
} |
|
STL итераторы идут дальше и определяют несколько категорий итераторов (forward, bidirectional, random access), обеспечивая разные уровни функциональности. Но базовый принцип тот же — предоставить унифицированный способ перебора элементов, не раскрывая деталей реализации контейнера.
Посредник (Mediator)
Когда объекты слишком сильно связаны между собой, паттерн "Посредник" приходит на помощь. Он централизует сложную логику взаимодействия, уменьшая связанность между объектами. Представьте чат-комнату, где пользователи общаются друг с другом:
C++ | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
| // Интерфейс посредника
class ChatMediator {
public:
virtual void sendMessage(const std::string& msg, const std::string& userId) = 0;
virtual void addUser(class User* user) = 0;
virtual ~ChatMediator() = default;
};
// Абстрактный пользователь
class User {
protected:
ChatMediator* mediator;
std::string id;
public:
User(ChatMediator* mediator, const std::string& id)
: mediator(mediator), id(id) {}
std::string getId() const {
return id;
}
virtual void send(const std::string& msg) {
mediator->sendMessage(msg, id);
}
virtual void receive(const std::string& msg) = 0;
virtual ~User() = default;
};
// Конкретный пользователь
class ChatUser : public User {
public:
using User::User;
void receive(const std::string& msg) override {
std::cout << id << " received: " << msg << std::endl;
}
};
// Конкретный посредник
class ChatRoom : public ChatMediator {
private:
std::unordered_map<std::string, User*> users;
public:
void addUser(User* user) override {
users[user->getId()] = user;
}
void sendMessage(const std::string& msg, const std::string& userId) override {
for (auto& pair : users) {
if (pair.first != userId) {
pair.second->receive(msg);
}
}
}
}; |
|
Использование:
C++ | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| ChatRoom chatroom;
ChatUser* user1 = new ChatUser(&chatroom, "Alice");
ChatUser* user2 = new ChatUser(&chatroom, "Bob");
ChatUser* user3 = new ChatUser(&chatroom, "Charlie");
chatroom.addUser(user1);
chatroom.addUser(user2);
chatroom.addUser(user3);
user1->send("Hello, everyone!"); // Bob и Charlie получат сообщение
user2->send("Hi, Alice!"); // Alice и Charlie получат сообщение
delete user1;
delete user2;
delete user3; |
|
Посредник особенно полезен в GUI приложениях, где множество компонентов должны взаимодействовать друг с другом. Вместо прямых связей между компонентами, все общение идет через посредника, что делает систему более гибкой и менее связанной.
Снимок (Memento)
Паттерн "Снимок" позволяет сохранять и восстанавливать предыдущее состояние объекта без нарушения инкапсуляции. Это идеальное решение для реализации функции "отмены" (undo) в приложениях.
C++ | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
| // Снимок - объект, хранящий состояние редактора
class EditorMemento {
private:
std::string content;
public:
explicit EditorMemento(const std::string& content) : content(content) {}
std::string getContent() const {
return content;
}
};
// Создатель снимков - текстовый редактор
class TextEditor {
private:
std::string content;
public:
void write(const std::string& words) {
content += words;
}
std::string getContent() const {
return content;
}
// Создаёт снимок текущего состояния
EditorMemento save() const {
return EditorMemento(content);
}
// Восстанавливает состояние из снимка
void restore(const EditorMemento& memento) {
content = memento.getContent();
}
};
// Опекун - хранит историю снимков
class History {
private:
std::vector<EditorMemento> mementos;
public:
void push(const EditorMemento& memento) {
mementos.push_back(memento);
}
EditorMemento pop() {
auto memento = mementos.back();
mementos.pop_back();
return memento;
}
}; |
|
Какая прелесть этого паттерна — полное отделение механизма сохранения истории от основного класса. История ничего не знает о внутренностях TextEditor , а сам редактор не заботится о том, где и как хранятся снимки.
Наблюдатель (Observer)
"Наблюдатель" — один из самых востребованных паттернов в программировании событийно-ориентированных систем. Он определяет зависимость "один-ко-многим" между объектами, так что при изменении одного объекта все зависимые от него автоматически уведомляются. Вот пример системы оповещения о новых сообщениях:
C++ | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
| // Абстрактный наблюдатель
class Observer {
public:
virtual void update(const std::string& message) = 0;
virtual ~Observer() = default;
};
// Конкретные наблюдатели
class EmailNotifier : public Observer {
private:
std::string email;
public:
EmailNotifier(const std::string& email) : email(email) {}
void update(const std::string& message) override {
std::cout << "Sending email to " << email << ": " << message << std::endl;
}
};
class SMSNotifier : public Observer {
private:
std::string phoneNumber;
public:
SMSNotifier(const std::string& phoneNumber) : phoneNumber(phoneNumber) {}
void update(const std::string& message) override {
std::cout << "Sending SMS to " << phoneNumber << ": " << message << std::endl;
}
};
// Издатель (наблюдаемый объект)
class MessagePublisher {
private:
std::vector<Observer*> observers;
public:
void attach(Observer* observer) {
observers.push_back(observer);
}
void detach(Observer* observer) {
observers.erase(std::remove(observers.begin(), observers.end(), observer), observers.end());
}
void notify(const std::string& message) {
for (Observer* observer : observers) {
observer->update(message);
}
}
void createMessage(const std::string& message) {
std::cout << "New message created: " << message << std::endl;
notify(message);
}
}; |
|
В C++11 и новее можно реализовать этот паттерн более элегантно с помощью функциональных объектов, лямбд и std::function. За годы практики я заметил, что такой подход более гибкий, чем классический интерфейс Observer:
C++ | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| class ModernPublisher {
private:
std::vector<std::function<void(const std::string&)>> callbacks;
public:
void subscribe(std::function<void(const std::string&)> callback) {
callbacks.push_back(callback);
}
void publish(const std::string& message) {
for (const auto& callback : callbacks) {
callback(message);
}
}
}; |
|
Состояние (State)
Как часто вы сталкивались с монструозными условными операторами, проверяющими текущее состояние объекта? Паттерн "Состояние" позволяет объекту изменять своё поведение при изменении внутреннего состояния, инкапсулируя логику каждого состояния в отдельный класс. Представьте, что мы моделируем торговый автомат с напитками:
C++ | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| // Контекст — торговый автомат
class VendingMachine;
// Абстрактное состояние
class State {
protected:
VendingMachine* vendingMachine;
public:
virtual void insertCoin() = 0;
virtual void ejectCoin() = 0;
virtual void selectDrink() = 0;
virtual void dispense() = 0;
virtual ~State() = default;
void setVendingMachine(VendingMachine* machine) {
vendingMachine = machine;
}
};
// Конкретные состояния будут определены после класса контекста |
|
Самое изящное в этом паттерне то, что мы полностью избавляемся от условной логики в самом объекте. Каждое состояние знает, как себя вести, и какое состояние должно следовать за ним.
Примеры и рекомендации по выбору шаблонов
Первое и самое главное правило: не используйте шаблоны проектирования просто чтобы их использовать. Новички часто впадают в ловушку "паттерномании", когда каждый кусок кода оборачивается в какой-нибудь паттерн. Помните мудрость Кента Бека: "Сначала сделайте так, чтобы работало; потом сделайте правильно; и только потом сделайте быстро." Шаблоны проектирования относятся к шагу "сделать правильно", но только если это действительно необходимо. Вот несколько типичных ситуаций и подходящие для них шаблоны:
1. Когда необходимо создавать объекты, но заранее неизвестен их конкретный тип:
- Фабричный метод — когда один класс не знает, какие именно субклассы ему нужно создавать.
- Абстрактная фабрика — когда нужно создавать семейства взаимосвязанных объектов.
2. Когда нужно контролировать доступ к объекту:
- Заместитель — для контроля доступа к оригинальному объекту.
- Фасад — для упрощения сложного интерфейса.
3. Когда часто меняется поведение объекта:
- Стратегия — когда нужно изменять алгоритм независимо от клиентов.
- Состояние — когда поведение объекта зависит от его внутренего состояния.
- Декоратор — когда нужно динамически добавлять функциональность объекту.
На моём последнем проекте — библиотеке обработки финансовых транзакций — мы столкнулись с интересной задачей: система должна была поддерживать разные форматы входных данных (JSON, XML, CSV) и разные протоколы передачи (REST, SOAP, файловый обмен). Мой коллега предложил использовать огромный оператор switch по всему коду... К счастью, комбинация Стратегии (для алгоритмов парсинга) и Абстрактной фабрики (для создания парсеров под разные протоколы) спасла проект от превращения в спагетти-код.
Распространённая ошибка, с которой я сталкивался многократно, — использование Одиночки (Singleton) как глобальной переменной. Одиночка имеет свои применения, но часто является признаком плохого дизайна. Особенно в многопоточной среде она может создать настоящий кошмар для отладки.
C++ | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
| // Антипаттерн: Синглтон как глобальная переменная
class GlobalConfig {
private:
static GlobalConfig* instance;
std::map<std::string, std::string> settings;
GlobalConfig() {}
public:
static GlobalConfig* getInstance() {
if (instance == nullptr) {
instance = new GlobalConfig();
}
return instance;
}
void setSetting(const std::string& key, const std::string& value) {
settings[key] = value;
}
std::string getSetting(const std::string& key) {
return settings[key];
}
};
// Инициализация статической переменной
GlobalConfig* GlobalConfig::instance = nullptr;
// Использование в разных частях программы:
void someFunction() {
GlobalConfig::getInstance()->setSetting("timeout", "30");
}
void anotherFunction() {
std::string timeout = GlobalConfig::getInstance()->getSetting("timeout");
} |
|
Вместо этого лучше использовать внедрение зависимостей:
C++ | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
| class Config {
public:
virtual std::string getSetting(const std::string& key) = 0;
virtual void setSetting(const std::string& key, const std::string& value) = 0;
virtual ~Config() = default;
};
class FileConfig : public Config {
// Реализация
};
class DbConfig : public Config {
// Реализация
};
class Service {
private:
Config& config;
public:
Service(Config& cfg) : config(cfg) {}
void doSomething() {
std::string timeout = config.getSetting("timeout");
// Использование timeout
}
}; |
|
Когда выбираете между похожими шаблонами, обращайте внимание на их тонкие различия:
Стратегия vs Состояние: Стратегия обычно устанавливается извне и не меняется во время жизни объекта, тогда как Состояние может менять само себя.
Декоратор vs Заместитель: Декоратор добавляет поведение, не меняя интерфейс; Заместитель контролирует доступ к объекту.
Адаптер vs Фасад: Адаптер меняет интерфейс существующего объекта; Фасад предоставляет новый, упрощённый интерфейс для набора объектов.
Ещё одна рекомендация: не бойтесь комбинировать шаблоны. В реальных проектах часто используется несколько шаблонов вместе. Например, Наблюдатель + Команда для создания системы отмены действий с уведомлением всех заинтересованных компонентов.
При выборе шаблона всегда думайте о будущем развитии проекта. Какие требования могут измениться? Какая часть дизайна будет оставаться стабильной? Хороший архитектурный дизайн делает изменяемые части легко заменяемыми, а стабильные — крепким фундаментом системы.
Реализация шаблонов класса в инлайн файле Пытался написать шаблонный класс, реализуя как обычно объявление класса в h файле и определение... Реализация шаблонов Всем привет! Можно ли как-то реализовывать шаблонный класс в другом файле?
то есть имеем... Реализация шаблонов underline_type и is_enum Добрый вечер!
Кто подскажет, как сделать шаблоны underline_type и is_enum?
Смотрел type_traits,... Шаблоны проектирования В чем собственно вопрос используете ли вы паттерны проектирования в своих проектах? Паттерны(шаблоны проектирования) Народ,возникла проблема..не могу выбрать паттерн для своей темы по курсачу...помогите плиз..с идеей... Паттерны (шаблоны) проектирования Доброго время суток. Надо реальная программа с описанием используемых паттернов в ней. Можите... Пакеты для проектирования БИС? Я слышал, что С++ используется для проектирования БИС.
Кто-нибудь знает, какие библиотеки/пакеты... ООП реализованная через паттерны проектирования Друзья мои дорогие, очень прошу , если у кого есть готовые приложения на языке С++ где можно... Шаблоны проектирования Шаблоны проектирования, их реализация на С++.
Кто знает какие-то хорошие книги, поделитесь :) шаблоны проектирования здавствуйте. помогите, пожалуйста, придумать фрагмент кода программы с использованием шаблона... Паттерн проектирования «Фасад» Здравствуйте. Можете по простому объяснить про паттерн проектирования «Фасад», его плюсы, минусы,... Чем плох паттерн проектирования Singleton? Доброго, программисты.
Вот многие пишут, что этот паттерн плох сам по себе.
Но я не пойму почему?...
|