Программирование на основе классов и шаблонов

Шаблоны проектирования

Аладин Дмитрий Владимирович

iu5edu.ru/wiki/cpp2

План

  1. Антипаттерны.
  2. Архитектурные антипаттерны.
  3. Антипаттерны разработки.
  4. Связь архитектуры приложения и шаблонов проектирования.
  5. Шаблоны проектирования и их классификация.
  6. UML-диаграммы и шаблоны проектирования.
  7. Порождающие шаблоны: Одиночка, Фабричный метод, Строитель.
  8. Структурные шаблоны: Адаптер, Фасад.
  9. Поведенческие шаблоны: Состояние.

Следующий материал успешно украден адаптирован с хабра, причем несколько раз: первый, второй, третий, четвертый и т.д. А еще взят материал с doka.guide

Другие источники смотрите в "дополнительном материале".

Адаптированный материал публикуется с сохранением условий распространения.

Если вы по коридору
Мчитесь на велосипеде,
А на встречу вам из ванной
Вышел папа погулять,
Не сворачивайте в кухню,
В кухне твердый холодильник.
Тормозите лучше в папу.
Папа мягкий. Он простит.

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

P.S. Не кажутся ли вам, что эти строчки знакомы?

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

... вредные советы рекомендуется читать всем детям — и послушным, и непослушным.

У программистов тоже есть "вредные советы". И называют их "антипаттерны".

Если паттерны (шаблоны) проектирования — это примеры практик хорошего программирования, то есть шаблоны решения определённых задач. То антипаттерны — их полная противоположность, это — шаблоны ошибок, которые совершаются при решении различных задач.

Частью практик хорошего программирования является именно избежание антипаттернов (привет, кэп)!

Не менее ценно умение приручить и контролировать антипаттерны.

Антипаттерны в дикой природе

Можете встретить:

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

Инверсия абстракции

Попытка реализации низкоуровневых конструкций поверх высокоуровневых.

center

P.S. С какими проблемами можно столкнуться в верхней цепочке?

Большой ком грязи (aka божественный объект)

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

center

Возможных причин появления антипаттерна:

  1. Недостаток компетенций в проектировании.
  2. Нехватка времени или бюджета.
  3. Изменение требований.
  4. Текучка кадров.
  5. Рост системы и технического долга.
  6. Унаследованная сложность (проработка предметной области ↓ ⇒ качество архитектуры ↓).

P.S. У любой проблемы в программировании можно найти ворох причин, а на ее исправление может повлиять тройственная ограниченность (стоимость, время и качество).

Замкнутость на поставщике (Vendor lock-in)

Архитектура, основанная на определенных продуктах. Под продуктом тут имеется в виду определенная база данных, сервис или технология. Vendor-ом называется поставщик таких продуктов.

Защита своих активов (Cover your Assets)

Предоставление набора вариантов вместо конкретного решения или вовсе избегания принятия решения под любым предлогом.

Тесно с этим связан "аналитический паралич" (организационный антипаттерн) - чрезмерное анализирование ситуации при планировании, так что решение или действие не предпринимаются, по сути парализуя разработку.

Программирование копи-пастом (Copy and Paste Programming)

Когда от программиста требуется написание двух схожих функций, самым "простым" решением является написание одной функции, её копирование и внесение некоторых изменений в копию.

P.S. Какие проблемы это сулит?

"Брось, можно писать не только одну функцию!" или Спагетти-код (Spaghetti code)

слабо структурированная и плохо спроектированная система, запутанная и очень сложная для понимания.

"Золотое правило": проиграв немного времени сейчас — вы получите огромный плюс в будущем. Или наоборот — проиграете, если оставите спагетти-код в проекте.

Золотой молоток (Golden hammer)

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

center

"Что за 42?" или Магические числа (Magic numbers)

Магическое число — константа, использованная в коде для чего либо (чаще всего — идентификации данных), само число не несёт никакого смысла без соответствующего комментария.

"Что значит d:\proj\tests.dat?" или Жёсткое кодирование (Hard code)

— внедрение различных данных об окружении в реализацию.

Мягкое кодирование (Soft code)

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

Антипаттернов гораздо больше и узнать о них можно тут или тут. Про "военные" преступления в программировании тут.

center

Шаблоны проектирования = хороший код?

center

⚠️ Шаблоны проектирования ≠ хороший код!

  • Шаблоны проектирования — не "серебряная пуля".
  • Не пытайтесь внедрять их принудительно, последствия могут быть негативными. Помните, что шаблоны — это способы решения, а не поиска проблем. Так что не перемудрите.
  • Если применять их правильно и в нужных местах, они могут оказаться спасением. В противном случае у вас будет ещё больше проблем.

Шаблоны проектирования

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

Связь архитектуры приложения и шаблонов проектирования

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

Архитектура включает в себя подходы: ограничения, правила и эвристики, которым надо следовать при написании кода.

Шаблон проектирования — шаблонное решение частой архитектурной проблемы.

Область ответственности шаблонов проектирования меньше, чем у архитектуры в целом:

  • Шаблоны помогают нам решать проблемы на более "низком уровне" ближе к непосредственно коду.
  • Архитектура же решает проблемы проектирования всей системы в целом.

Если упростить, то:

  • архитектура — это инструкция "Как построить дом в общем", она охватывает целый проект.
  • шаблоны — инструкции для конкретных задач: "Как забить сваи", "Как замешать цемент", "Как проложить проводку".

Design Patterns

"Приёмы объектно-ориентированного проектирования. Паттерны проектирования" — книга 1994 года о программной инженерии, описывающая шаблоны проектирования программного обеспечения.

Авторами книги, которых прозвали "Бандой четырёх", являются Эрих Гамма, Ричард Хелм, Ральф Джонсон, Джон Влиссидес.

Предисловие написал Гради Буч.

Книга состоит из двух частей, в первых двух главах рассказывается о возможностях и недостатках объектно-ориентированного программирования, а во второй части описаны 23 классических шаблона проектирования. Примеры в книге написаны на языках программирования C++ и Smalltalk.

center

Расширенное представление о шаблонах проектирования

  • Основные (порождающие, структурные, поведенческие)
  • Частные
    • Шаблоны параллельного программирования
    • Шаблоны генерации объектов
    • Шаблоны программирования гибких объектов
    • Шаблоны выполнения задач
    • Шаблоны архитектуры системы
    • Enterprise
    • Шаблоны проектирования потоковой обработки
    • Шаблоны проектирования распределённых систем
    • Шаблоны Баз Данных
    • Прочие
  • Другие типы шаблонов

Порождающие шаблоны (Creational)

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

  • Фабрика;
  • Фабричный метод; ← рассмотрим его
  • Абстрактная фабрика;
  • Строитель; ← рассмотрим его
  • Одиночка. ← рассмотрим его

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

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

  • Адаптер; ← рассмотрим его
  • Декоратор;
  • Фасад; ← рассмотрим его
  • Прокси.

— отвечают на вопрос "Как составить программный компонент?"

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

распределяют ответственность между модулями и определяют, как именно будет происходить общение. Примеры:

  • Цепочка ответственности;
  • Стратегия;
  • Команда;
  • Наблюдатель;
  • Состояние. ← рассмотрим его

— отвечают на вопрос "Как организовать поведение программного компонента?"

UML-диаграммы и шаблоны проектирования

Отношения между классами:

center

💍 (C) Одиночка (Singleton)

Аналогия

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

Вкратце

Шаблон позволяет удостовериться, что создаваемый объект — единственный в своём классе.

Примечание

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

💍 (C) Одиночка: UML-диаграмма

center

💍 (C) Одиночка: Пример

class Singleton {
  Singleton() {}
  static Singleton* instance;
 public:
  Singleton(Singleton const&) = delete;
  Singleton& operator=(Singleton const&) = delete;

  static Singleton* get() {
    if (!instance) {
      instance = new Singleton();
    }
    return instance;
  }

  static void restart() {
    if (instance) {
      delete instance;
    }
  }

  void tell() { std::cout << "This is Singleton." << std::endl; }
};

Пример использования:

Singleton* Singleton::instance = nullptr;

int main() {
  Singleton::get()->tell();
  Singleton::restart();

  return 0;
}

Предпочтительный сценарий

Такой сценарий, где должен существовать только один экземпляр класса (в том числе учитывая кэши, пулы потоков и реестры).

Ограничение

В текущем виде использование объекта не потокобезопасно. Можно ввести синхронизацию, но так замедлится выполнение.

P.S. Мало ктно знает, но правильное название шаблона: синглтрон!

🏭 (C) Фабричный метод (Factory Method)

Аналогия

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

Вкратце

Это способ делегирования логики создания объектов (instantiation logic) дочерним классам.

🏭 (C) Фабричный метод: UML-диаграмма

center

🏭 (C) Фабричный метод: Пример

class Product {
 public:
  virtual ~Product() {}
  virtual std::string getName() = 0;
};

class ConcreteProductA : public Product {
 public:
  ~ConcreteProductA() {}
  std::string getName() { return "type A"; }
};

class ConcreteProductB : public Product {
 public:
  ~ConcreteProductB() {}
  std::string getName() { return "type B"; }
};
class Creator {
 public:
  virtual ~Creator() {}
  virtual Product *createProductA() = 0;
  virtual Product *createProductB() = 0;
  virtual void removeProduct(Product *product) = 0;
};

class ConcreteCreator : public Creator {
 public:
  ~ConcreteCreator() {}
  Product *createProductA() { return new ConcreteProductA(); }
  Product *createProductB() { return new ConcreteProductB(); }
  void removeProduct(Product *product) { delete product; }
};

Пример использования:

int main() {
  Creator *creator = new ConcreteCreator();

  Product *p1 = creator->createProductA();
  std::cout << "Product: " << p1->getName() << std::endl;
  creator->removeProduct(p1);

  Product *p2 = creator->createProductB();
  std::cout << "Product: " << p2->getName() << std::endl;
  creator->removeProduct(p2);

  delete creator;
  return 0;
}

Предпочтительный сценарий

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

Ограничение

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

👷‍♂️ (C) Строитель (Builder)

Аналогия

Допустим, вы пришли в забегаловку, заказали бургер дня, и вам выдали его без вопросов. Это пример "Простой фабрики". Но иногда логика создания состоит из большего количества шагов. К примеру, при заказе бургера дня есть несколько вариантов хлеба, начинки, соусов, дополнительных ингредиентов. В таких ситуациях помогает шаблон "Строитель".

Вкратце

Шаблон позволяет создавать разные свойства объекта, избегая загрязнения конструктора (constructor pollution). Это полезно, когда у объекта может быть несколько свойств. Или когда создание объекта состоит из большого количества этапов.

👷‍♂️ (C) Строитель: UML-диаграмма

center

👷‍♂️ (C) Строитель: Пример

class Product {
  std::string partA;
  std::string partB;

 public:
  void makeA(const std::string &part) { partA = part; }
  void makeB(const std::string &part) { partB = part; }
  std::string get() { return (partA + " " + partB); }
};

class Builder {
 protected:
  Product product;

 public:
  virtual ~Builder() {}
  Product get() { return product; }
  virtual void buildPartA() = 0;
  virtual void buildPartB() = 0;
};
class ConcreteBuilderX : public Builder {
 public:
  void buildPartA() { product.makeA("A-X"); }
  void buildPartB() { product.makeB("B-X"); }
};

class ConcreteBuilderY : public Builder {
 public:
  void buildPartA() { product.makeA("A-Y"); }
  void buildPartB() { product.makeB("B-Y"); }
};
class Director {
  Builder *builder;

 public:
  Director() : builder() {}
  ~Director() {
    if (builder) {
      delete builder;
    }
  }
  void set(Builder *b) {
    if (builder) {
      delete builder;
    }
    builder = b;
  }
  Product get() { return builder->get(); }
  void construct() {
    builder->buildPartA();
    builder->buildPartB();
  }
};

Пример использования:

int main() {
  Director director;
  director.set(new ConcreteBuilderX);
  director.construct();

  Product product1 = director.get();
  std::cout << "1st product parts: " << product1.get() << std::endl;

  director.set(new ConcreteBuilderY);
  director.construct();

  Product product2 = director.get();
  std::cout << "2nd product parts: " << product2.get() << std::endl;

  return 0;
}

Предпочтительный сценарий

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

Ограничение

Количество строк кода увеличивается, по крайней мере, вдвое в шаблоне строителя, но усилия окупаются с точки зрения гибкости дизайна и гораздо более читаемого кода.

🔌 (S) Адаптер (Adapter)

Аналогия

Допустим, у вас на карте памяти есть какие-то картинки. Их нужно перенести на компьютер. Нужен адаптер, совместимый с входным портом компьютера, в который можно вставить карту памяти. В данном примере адаптер — это картридер. Ещё один пример: переходник, позволяющий использовать американский блок питания с российской розеткой. Третий пример: переводчик — это адаптер, соединяющий двух людей, говорящих на разных языках.

Вкратце

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

🔌 (S) Адаптер: UML-диаграмма

center

🔌 (S) Адаптер: Пример

Через наследование:

class Target {
 public:
  virtual ~Target() {}
  virtual void request() = 0;
};

class Adaptee {
 public:
  ~Adaptee() {}
  void specificRequest() { std::cout << "specific request" << std::endl; }
};

class Adapter : public Target, private Adaptee {
 public:
  virtual void request() { specificRequest(); }
};

Через композицию:

class Target {
 public:
  virtual ~Target() {}
  virtual void request() = 0;
};

class Adaptee {
 public:
  void specificRequest() { std::cout << "specific request" << std::endl; }
};

class Adapter : public Target {
  Adaptee *adaptee;

 public:
  Adapter() : adaptee() {}
  ~Adapter() { delete adaptee; }
  void request() { adaptee->specificRequest(); }
};

Пример использования:

int main() {
  Target *t = new Adapter();
  t->request();
  delete t;

  return 0;
}

Предпочтительный сценарий

В случае двух приложений, одно из которых выдает выходные данные в формате XML, а другое требует ввод JSON (или какого-то другого формата), для бесперебойного контакта между ними вам понадобится адаптер.

Ограничение

Адаптер не сочетается с подклассами Adaptee или Target.

📦 (S) Фасад (Facade)

Аналогия

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

Вкратце

Шаблон "Фасад" предоставляет упрощённый интерфейс для сложной подсистемы.

📦 (S) Фасад: UML-диаграмма

center

📦 (S) Фасад: Пример

class SubsystemA {
 public:
  void suboperation() { std::cout << "Subsystem A method" << std::endl; }
};

class SubsystemB {
 public:
  void suboperation() { std::cout << "Subsystem B method" << std::endl; }
};

class Facade {
  SubsystemA *subsystemA;
  SubsystemB *subsystemB;

 public:
  Facade() : subsystemA(), subsystemB() {}
  void operation1() { subsystemA->suboperation(); }
  void operation2() { subsystemB->suboperation(); }
};

Пример использования:

int main() {
  Facade *facade = new Facade();

  facade->operation1();
  facade->operation2();
  delete facade;

  return 0;
}

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

💢 (B) Состояние (State)

Аналогия

Допустим, в графическом редакторе вы выбрали инструмент "Кисть". Она меняет своё поведение в зависимости от настройки цвета: т.е. рисует линию выбранного цвета.

Вкратце

Шаблон позволяет менять поведение класса при изменении состояния.

💢 (B) Состояние: UML-диаграмма

center

💢 (B) Состояние: Пример

class State {
 public:
  virtual ~State() { /* ... */ }
  virtual void handle() = 0;
};

class ConcreteStateA : public State {
 public:
  ~ConcreteStateA() { /* ... */ }
  void handle() { std::cout << "State A handled." << std::endl; }
};

class ConcreteStateB : public State {
 public:
  ~ConcreteStateB() { /* ... */ }
  void handle() { std::cout << "State B handled." << std::endl; }
};
class Context {
  State *state;

 public:
  Context() : state() { /* ... */  }

  ~Context() { delete state; }

  void setState(State *const s) {
    if (state) {
      delete state;
    }
    state = s;
  }

  void request() { state->handle(); }
};

Пример использования:

int main() {
  Context *context = new Context();

  context->setState(new ConcreteStateA());
  context->request();

  context->setState(new ConcreteStateB());
  context->request();

  delete context;
  return 0;
}

Предпочтительный сценарий

Случай, когда нужно представить несколько состояний объекта, способного к внутренним изменения. Если обходиться без шаблона состояния, код становится негибким и слишком полагается на структуру if-else.

Ограничение

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

Дальнейшее изучение шаблонов проектирования

17 оставшихся шаблонов (паттернов) предлагается изучить в:

Помимо этого предлагается самостоятельно изучить:

Вопросы?

center

If not, just clap your hands!