Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Lecture №2.8. Design patterns (part 1). Creatio...

Lecture №2.8. Design patterns (part 1). Creational patterns.

1. Design pattern types.
2. Singleton.
3. Builder.
4. Factory method.
5. Prototype.

Avatar for Baramiya Denis

Baramiya Denis

October 22, 2019

More Decks by Baramiya Denis

Other Decks in Programming

Transcript

  1. Зачем нужны паттерны Похожие задачи встречаются снова и снова —

    решение уже придумано Общий язык: фразу «здесь нужен Builder» поймёт вся команда Узнать знакомый шаблон в задаче — уже половина решения
  2. Из чего состоит паттерн Имя Короткое название: «Одиночка», «Строитель» Задача

    В какой ситуации паттерн нужен Решение Какие классы нужны и как они связаны Результаты Что выигрываем и чем за это платим
  3. Виды паттернов Порождающие Структурные Поведенческие Как создавать объекты Сегодня Как

    собирать из объектов более крупные структуры Как объекты общаются и делят обязанности
  4. Пишем графический редактор Мой редактор — рисунок.svg Прямоугольник Круг Текст

    Каждую задачу сначала решим в лоб, найдём проблему — и исправим паттерном
  5. Порождающий паттерн · 1 / 5 Singleton Одиночка У класса

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

    холсту, панелям, экспорту Должны быть одни на всё приложение
  7. В лоб: глобальная переменная extern Settings g_settings; Settings g_settings; //

    settings.h: объявление // settings.cpp: определение Ничто не мешает создать второй Settings Глобальные объекты из разных .cpp создаются в неопределённом порядке
  8. Как часто пишут Singleton class Settings { public: static Settings*

    instance() { if (!instance_) instance_ = new Settings(); return instance_; } // два потока → два объекта // кто вызовет delete? private: Settings() = default; static Settings* instance_; }; Settings* Settings::instance_ = nullptr; Утечка: delete не вызовут Два потока → два объекта
  9. Singleton Мейерса class Settings { public: static Settings& instance() {

    static Settings settings; return settings; } // при первом вызове Settings(const Settings&) = delete; Settings& operator=(const Settings&) = delete; C++11: создание staticпеременной потокобезопасно Копировать нельзя bool showGrid() const { return showGrid_; } void setShowGrid(bool on) { showGrid_ = on; } private: Settings() = default; bool showGrid_ = true; }; Удалится сам при выходе из программы
  10. Проблема: скрытая зависимость void Canvas::draw() const { if (Settings::instance().showGrid()) drawGrid();

    drawShapes(); } // снаружи этого не видно По объявлению void draw() const не видно, что метод читает настройки
  11. Чем плох Singleton Не видно От настроек зависит любой код

    — по его объявлению этого не понять Тесты Один тест выключил сетку — она выключена и в следующем Подмена В тесте не подставить свои настройки: класс один, и он читает настоящий файл
  12. Решение: зависеть от интерфейса class ISettings { public: virtual ~ISettings()

    = default; virtual bool showGrid() const = 0; }; class FileSettings final : public ISettings { public: explicit FileSettings(const std::string& path); // читает файл bool showGrid() const override { return showGrid_; } private: bool showGrid_ = true; }; Холст знает только ISettings FileSettings читает файл
  13. Холст получает настройки снаружи class Canvas { public: explicit Canvas(std::shared_ptr<const

    ISettings> settings) : settings_(std::move(settings)) {} void draw() const { if (settings_->showGrid()) drawGrid(); drawShapes(); } private: void drawGrid() const { std::cout << "grid\n"; } void drawShapes() const { std::cout << "shapes\n"; } std::shared_ptr<const ISettings> settings_; };
  14. Один экземпляр создаёт main() // main(): один объект на всё

    приложение auto settings = std::make_shared<FileSettings>("settings.txt"); Canvas canvas(settings); Canvas preview(settings); // тот же самый объект Объект по-прежнему один, но класс об этом не заботится
  15. Подмена в тестах class FakeSettings final : public ISettings {

    public: explicit FakeSettings(bool grid) : showGrid_(grid) {} bool showGrid() const override { return showGrid_; } private: bool showGrid_; }; // тест: сетка выключена только здесь auto settings = std::make_shared<FakeSettings>(false); Canvas canvas(settings); canvas.draw(); // другие тесты не задеты
  16. Singleton как время жизни Время жизни Сколько объектов В редакторе

    Singleton Один на всё приложение Настройки, журнал Scoped Один на документ (сессию, запрос) История отмены Transient Новый при каждом запросе Фигура, команда Так это называют DI-контейнеры: AddSingleton / AddScoped / AddTransient в .NET, Boost.DI и Google Fruit в C++
  17. Итоги: Singleton Достоинства Ровно один экземпляр Создаётся, только когда нужен

    Версия Мейерса — короткая и потокобезопасная Недостатки Скрытые зависимости Тесты влияют друг на друга Не подменить в тестах Нужен один объект? Создайте его в main() и передайте через интерфейс
  18. Порождающий паттерн · 2 / 5 Factory Method Фабричный метод

    Базовый класс решает, когда создать объект, а подкласс — какой и как
  19. Задача: инструменты рисования Пользователь тянет мышью — появляется фигура Прямоугольник

    — по двум углам Круг — центр и точка на окружности Инструментов со временем станет больше
  20. Фигуры class Shape { public: virtual ~Shape() = default; virtual

    void draw() const = 0; }; class Rectangle final : public Shape { public: Rectangle(Point corner1, Point corner2); void draw() const override; }; class Circle final : public Shape { public: Circle(Point center, int radius); void draw() const override; };
  21. Решение в лоб void Editor::onDrag(Point from, Point to) { std::unique_ptr<Shape>

    shape; if (tool_ == ToolType::Rectangle) { shape = std::make_unique<Rectangle>(from, to); } else if (tool_ == ToolType::Circle) { shape = std::make_unique<Circle>(from, distance(from, to)); } canvas_.add(std::move(shape)); } Новый инструмент → правим Editor Такие if расползаются по коду Забыли ветку → shape == nullptr
  22. Идея: инструмент сам создаёт фигуру Создатель Tool Продукт возвращает onDrag()

    createShape() = 0 Shape draw() = 0 onDrag() вызывает createShape() CircleTool createShape() создаёт Circle draw()
  23. Базовый инструмент class Tool { public: virtual ~Tool() = default;

    // пользователь протянул мышью от from до to void onDrag(Canvas& canvas, Point from, Point to) const { auto shape = createShape(from, to); // фабричный метод canvas.add(std::move(shape)); } private: [[nodiscard]] virtual std::unique_ptr<Shape> createShape(Point from, Point to) const = 0; }; private virtual: наследник переопределяет, но не вызывает
  24. Конкретные инструменты class RectangleTool final : public Tool { private:

    std::unique_ptr<Shape> createShape(Point from, Point to) const override { return std::make_unique<Rectangle>(from, to); // два угла } }; class CircleTool final : public Tool { private: std::unique_ptr<Shape> createShape(Point from, Point to) const override { return std::make_unique<Circle>(from, distance(from, to)); } }; Новый инструмент — новый класс Tool и Editor не меняются
  25. Использование Canvas canvas; std::unique_ptr<Tool> tool = std::make_unique<RectangleTool>(); tool->onDrag(canvas, {0, 0},

    {200, 100}); // прямоугольник tool = std::make_unique<CircleTool>(); tool->onDrag(canvas, {50, 50}, {80, 50}); // круг радиусом 30 Сценарий один — фигуры и способ их построения разные
  26. Всегда ли нужен подкласс? Да Наследники отличаются логикой: углы прямоугольника

    против центра и радиуса круга Нет Наследники отличаются только типом фигуры — класс на каждую фигуру будет лишним Для второго случая есть шаблонная фабрика
  27. Шаблонная фабрика template <class TShape> class SimpleTool final : public

    Tool { private: std::unique_ptr<Shape> createShape(Point from, Point to) const override { return std::make_unique<TShape>(from, to); } }; Один шаблон вместо класса на каждую фигуру Тип фигуры — параметр шаблона
  28. Шаблонная фабрика: использование tool = std::make_unique<SimpleTool<Rectangle>>(); tool->onDrag(canvas, {0, 0}, {40,

    20}); tool = std::make_unique<SimpleTool<Ellipse>>(); tool->onDrag(canvas, {0, 0}, {40, 20}); Подходит фигурам с конструктором (from, to). У круга своя логика — для него остаётся CircleTool
  29. А begin() у контейнеров? vector::begin() и list::begin() создают каждый свой

    итератор Но наследования и virtual здесь нет — шаблонный код просто вызывает c.begin() Это статический («утиный») полиморфизм, а не классический Factory Method
  30. Задача: открыть файл рисунка rect 0 0 200 100 circle

    50 50 30 Тип фигуры — строка из файла Нужно: имя → новый объект
  31. Фабрика по имени class ShapeFactory { public: using Creator =

    std::function<std::unique_ptr<Shape>(std::istream&)>; void add(std::string name, Creator creator) { creators_.insert_or_assign(std::move(name), std::move(creator)); } [[nodiscard]] std::unique_ptr<Shape> create(const std::string& name, std::istream& in) const { auto it = creators_.find(name); if (it == creators_.end()) throw std::invalid_argument("Unknown shape: " + name); return it->second(in); } private: std::unordered_map<std::string, Creator> creators_; };
  32. Регистрация фигур factory.add("rect", [](std::istream& in) { Point a, b; in

    >> a.x >> a.y >> b.x >> b.y; return std::make_unique<Rectangle>(a, b); }); factory.add("circle", [](std::istream& in) { Point center; int radius = 0; in >> center.x >> center.y >> radius; return std::make_unique<Circle>(center, radius); }); Новая фигура — один вызов add() Каждая фигура сама читает свои параметры
  33. Загрузка файла std::istringstream file("rect 0 0 200 100\n" "circle 50

    50 30\n"); std::string name; while (file >> name) loaded.add(factory.create(name, file)); Неизвестное имя → исключение, а не nullptr
  34. Итоги: Factory Method Достоинства Tool не знает конкретных фигур Новый

    инструмент — новый класс, Tool не меняется Недостатки Класс на каждый вариант — если отличие только в типе, лучше шаблон или функция Логика создания разбросана по наследникам
  35. Порождающий паттерн · 3 / 5 Abstract Factory Абстрактная фабрика

    Создаёт целый набор объектов, которые должны подходить друг другу, не называя их конкретных классов
  36. Задача: светлая и тёмная тема OK Светлая OK Тёмная OK

    Так нельзя! Панель, кнопки, поля ввода — всё должно быть из одной темы
  37. Решение в лоб void Window::build() { if (theme_ == Theme::Dark)

    { add(std::make_unique<DarkButton>()); add(std::make_unique<DarkPanel>()); } else { add(std::make_unique<LightButton>()); add(std::make_unique<DarkPanel>()); // ошибка: не та тема! } } Проверка темы — в каждом окне Легко ошибиться: тёмная панель в светлой теме Новая тема → правим все окна
  38. Идея: одна фабрика на тему Клиент ThemeFactory Window createButton() =

    0 createPanel() = 0 LightTheme DarkTheme createButton() createPanel() createButton() createPanel() LightButton LightPanel DarkButton DarkPanel
  39. Элементы интерфейса class Button { public: virtual ~Button() = default;

    virtual void draw() const = 0; }; class DarkButton final : public Button { public: void draw() const override { std::cout << "dark button\n"; } }; // LightButton, Panel, LightPanel, DarkPanel — так же
  40. Фабрика темы class ThemeFactory { public: virtual ~ThemeFactory() = default;

    [[nodiscard]] virtual std::unique_ptr<Button> createButton() const = 0; [[nodiscard]] virtual std::unique_ptr<Panel> createPanel() const = 0; }; Один метод на каждый вид элемента
  41. Тёмная тема class DarkTheme final : public ThemeFactory { public:

    std::unique_ptr<Button> createButton() const override { return std::make_unique<DarkButton>(); } std::unique_ptr<Panel> createPanel() const override { return std::make_unique<DarkPanel>(); } }; LightTheme — так же, но со светлыми элементами
  42. Окно class Window { public: explicit Window(const ThemeFactory& theme) :

    button_(theme.createButton()), panel_(theme.createPanel()) {} void draw() const { panel_->draw(); button_->draw(); } private: std::unique_ptr<Button> button_; std::unique_ptr<Panel> panel_; }; Окно получает фабрику и не знает, какая сейчас тема
  43. Выбор темы — в одном месте std::unique_ptr<ThemeFactory> makeTheme(bool dark) {

    if (dark) return std::make_unique<DarkTheme>(); return std::make_unique<LightTheme>(); } auto theme = makeTheme(darkMode); Window window(*theme); window.draw(); // тема выбирается в одном месте // окно не знает, какая тема // dark panel, dark button
  44. Ещё пример: графические API Фабрика Буфер Текстура Шейдер Интерфейс RenderDevice

    Buffer Texture Shader OpenGL GLDevice GLBuffer GLTexture GLShader Vulkan VulkanDevice VulkanBuffer VulkanTexture VulkanShader Движок прячет разные API за общими интерфейсами. Остальной код работает только с Buffer, Texture, Shader — и не знает, какой API под ними Так устроены, например, Diligent Engine (IRenderDevice) и RHI в Unreal Engine
  45. Factory Method или Abstract Factory Factory Method Abstract Factory Создаёт

    Один объект Набор объектов, которые подходят друг другу Как Подкласс переопределяет метод Клиенту передают объект-фабрику В редакторе CircleTool → Circle DarkTheme → тёмные кнопка и панель Новый вариант Новый подкласс Новая фабрика
  46. Итоги: Abstract Factory Достоинства Элементы одной темы всегда подходят друг

    другу Тема меняется в одном месте Недостатки Новый вид элемента (например, Checkbox) → менять ThemeFactory и все темы Больше классов
  47. Порождающий паттерн · 4 / 5 Builder Строитель Собирает сложный

    объект по шагам. Один и тот же процесс сборки может давать разные результаты
  48. Решение в лоб TextStyle title("Arial", 24, true, false, true, 0x333333,

    Align::Center); Что значат true, false, true? Легко перепутать порядок аргументов Нужно передать все 7 параметров, даже если важен один
  49. Стиль текста class TextStyle { public: class Builder; std::string describe()

    const; private: TextStyle() = default; // для примера // только через Builder std::string font_ = "Arial"; int size_ = 12; bool bold_ = false; bool italic_ = false; bool underline_ = false; std::uint32_t color_ = 0x000000; Align align_ = Align::Left; }; Создать можно только через Builder У всех полей есть значения по умолчанию
  50. Строитель стиля class TextStyle::Builder { public: Builder& font(std::string f) {

    s_.font_ = std::move(f); return *this; } Builder& size(int pt) { s_.size_ = pt; return *this; } Builder& bold() { s_.bold_ = true; return *this; } Builder& italic() { s_.italic_ = true; return *this; } Builder& underline() { s_.underline_ = true; return *this; } Builder& color(std::uint32_t rgb) { s_.color_ = rgb; return *this; } Builder& align(Align a) { s_.align_ = a; return *this; } [[nodiscard]] TextStyle build() const { if (s_.size_ <= 0) throw std::invalid_argument("size must be > 0"); return s_; } private: TextStyle s_; };
  51. Использование TextStyle title = TextStyle::Builder() .font("Arial") .size(24) .bold() .underline() .color(0x333333)

    .align(Align::Center) .build(); TextStyle body = TextStyle::Builder().size(14).build(); Каждый параметр подписан Только нужные шаги // остальное по умолчанию build() проверяет стиль
  52. Задача: экспорт рисунка Один и тот же рисунок — в

    SVG и в текстовый отчёт Порядок обхода фигур один, а результат разный Это классический Builder из книги GoF: «один процесс — разные представления»
  53. Идея: распорядитель и строители Распорядитель Строитель exportLogo() ExportBuilder вызывает шаги

    addRectangle() addCircle() знает, что строить и в каком порядке SvgBuilder TextBuilder result(): SVG result(): текст знают, как записать результат
  54. Строитель и распорядитель class ExportBuilder { public: virtual ~ExportBuilder() =

    default; virtual void addRectangle(Point p, int w, int h) = 0; virtual void addCircle(Point center, int r) = 0; }; // Распорядитель: знает, ЧТО строить и в каком порядке void exportLogo(ExportBuilder& builder) { builder.addRectangle({0, 0}, 200, 100); builder.addCircle({100, 50}, 30); }
  55. SVG-строитель class SvgBuilder final : public ExportBuilder { public: void

    addRectangle(Point p, int w, int h) override { out_ << "<rect x='" << p.x << "' y='" << p.y << "' width='" << w << "' height='" << h << "'/>\n"; } void addCircle(Point c, int r) override { out_ << "<circle cx='" << c.x << "' cy='" << c.y << "' r='" << r << "'/>\n"; } std::string result() const { return out_.str(); } private: std::ostringstream out_; };
  56. Один процесс — разные результаты SvgBuilder svg; exportLogo(svg); std::cout <<

    svg.result(); // <rect .../> <circle .../> TextBuilder text; exportLogo(text); std::cout << text.result(); // Прямоугольник 200x100, Круг r=30
  57. Итоги: Builder Достоинства Понятно, что задаёт каждый параметр Проверка —

    в одном месте, в build() Один процесс — разные результаты Недостатки Ещё один класс Для 2–3 параметров — лишнее
  58. Задача: дублировать фигуру Выделили фигуру, нажали Ctrl+D — рядом появилась

    копия Редактор видит только Shape& Какой настоящий тип: Circle? Group?
  59. Решение в лоб void Editor::duplicateSelected() { // какой настоящий тип

    у *selected_? if (auto r = dynamic_cast<const Rectangle*>(selected_)) canvas_.add(std::make_unique<Rectangle>(*r)); else if (auto c = dynamic_cast<const Circle*>(selected_)) canvas_.add(std::make_unique<Circle>(*c)); // ...и так для каждой фигуры } Перебор всех типов Новая фигура → правим Editor Забыли тип → фигура не копируется
  60. Идея: фигура копирует себя сама Клиент Прототип Editor Shape duplicateSelected()

    clone() = 0 selected_->clone() Circle Group clone() clone() return make_unique<Circle>(*this);
  61. Прототип class Shape { public: virtual ~Shape() = default; [[nodiscard]]

    virtual std::unique_ptr<Shape> clone() const = 0; virtual void draw() const = 0; void moveTo(Point p) { pos_ = p; } Point position() const { return pos_; } Shape& operator=(const Shape&) = delete; protected: Shape() = default; Shape(const Shape&) = default; private: Point pos_; }; // только для наследников // общая часть всех фигур
  62. Конкретная фигура class Circle final : public Shape { public:

    explicit Circle(int radius) : radius_(radius) {} std::unique_ptr<Shape> clone() const override { return std::make_unique<Circle>(*this); } void draw() const override { std::cout << "Circle r=" << radius_ << '\n'; } private: int radius_; }; clone() вызывает копирующий конструктор самого Circle
  63. Дублирование void Editor::duplicateSelected() { if (!selected_) return; auto copy =

    selected_->clone(); // настоящий тип знать не нужно Point p = selected_->position(); copy->moveTo({p.x + 10, p.y + 10}); // чуть сдвинем canvas_.add(std::move(copy)); } Новая фигура — Editor не меняется
  64. Группа: глубокая копия class Group final : public Shape {

    public: Group() = default; Group(const Group& other) : Shape(other) { // позиция группы for (const auto& item : other.items_) items_.push_back(item->clone()); // глубокая копия } void add(std::unique_ptr<Shape> s) { items_.push_back(std::move(s)); } std::unique_ptr<Shape> clone() const override { return std::make_unique<Group>(*this); } void draw() const override; }; Shape(other) копирует базовую часть — позицию
  65. Задача: свои компоненты OK Образец OK OK OK Копии на

    холсте Пользователь собрал из фигур кнопку и сохранил её как компонент — как в Figma Новый «вид» объекта появился во время работы программы — класса для него нет
  66. Библиотека компонентов class ComponentLibrary { public: void save(std::string name, std::unique_ptr<Shape>

    sample) { samples_.insert_or_assign(std::move(name), std::move(sample)); } [[nodiscard]] std::unique_ptr<Shape> insert(const std::string& name) const { auto it = samples_.find(name); if (it == samples_.end()) throw std::invalid_argument("Unknown component: " + name); return it->second->clone(); } private: std::unordered_map<std::string, std::unique_ptr<Shape>> samples_; };
  67. Библиотека: использование auto button = std::make_unique<Group>(); // пользователь собрал кнопку

    button->add(std::make_unique<Rectangle>(120, 40)); button->add(std::make_unique<Text>("OK")); ComponentLibrary library; library.save("Кнопка", std::move(button)); auto b1 = library.insert("Кнопка"); auto b2 = library.insert("Кнопка"); // независимые копии Компонент «Кнопка» — это данные, а не класс в программе
  68. Проблема: одинаковый clone() // Circle return std::make_unique<Circle>(*this); // Rectangle return

    std::make_unique<Rectangle>(*this); // Group return std::make_unique<Group>(*this); Отличается только имя класса. Можно написать clone() один раз?
  69. CRTP template <class Derived> class Base { // здесь известен

    тип Derived }; class Triangle : public Base<Triangle> {}; Curiously Recurring Template Pattern — «странно повторяющийся шаблон» Класс наследуется от шаблона, параметризованного им самим База ещё при компиляции знает тип наследника
  70. CRTP: clone() один раз template <class Derived> class Clonable :

    public Shape { public: std::unique_ptr<Shape> clone() const override { const auto& self = static_cast<const Derived&>(*this); return std::make_unique<Derived>(self); } }; static_cast безопасен: объект на самом деле Derived make_unique<Derived> вызывает копирующий конструктор наследника
  71. CRTP: использование class Triangle final : public Clonable<Triangle> { public:

    void draw() const override { std::cout << "Triangle\n"; } }; class Star final : public Clonable<Star> { public: void draw() const override { std::cout << "Star\n"; } }; clone() больше не пишем руками
  72. Итоги: Prototype Достоинства Копируем, не зная настоящего типа Новые «виды»

    объектов — без новых классов Недостатки clone() нужен в каждом классе (CRTP помогает) Глубокое копирование сложных структур, например с циклическими ссылками
  73. Как выбрать Вопрос Паттерн В редакторе Объект нужен ровно один?

    Singleton или DI Settings Подкласс решает, что создать? Factory Method CircleTool Нужен набор подходящих друг другу объектов? Abstract Factory DarkTheme Много параметров или шагов? Builder TextStyle::Builder Нужна копия, а тип неизвестен? Prototype Shape::clone()
  74. Когда паттерн не нужен Один тип и нет планов расширения

    → обычный конструктор Два-три параметра → Builder не нужен «Хочу глобальную переменную» — это не повод для Singleton Паттерн — средство, а не цель