Шаблон шаблонного метода и невиртуальная идиома

Приведенное выше название также является одним из названий глав из книги «Паттерны проектирования на C++» Федора Пикуса. Мне так понравилась эта идея, что я быстро начал ее использовать, и мне захотелось поделиться некоторыми подробностями об этом паттерне и идиоме.

Но сначала давайте кратко обсудим, в чем разница между паттерном и идиомой. Если коротко, то паттерны не зависят от языка и относятся к дизайну, а идиомы зависят от языка и относятся к коду. Для получения более подробной информации ознакомьтесь с этой статьей.

Шаблон шаблонного метода

Прочитав заголовок, вы можете спросить, почему мы говорим и о шаблоне шаблонного метода (далее TMP), и о невиртуальной идиоме (далее NVI). TMP — это классический шаблон проектирования из книги Gang Of Four, а NVI — идиома, специфичная для C++.

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

stopTheCar();
plugTheFeed();
waitUntilEnoughFuelTransmitted();
unplugTheFeed();
Войти в полноэкранный режим Выход из полноэкранного режима

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

Как мы собираемся задействовать шаблоны C++ в этом решении? Ответ прост. Никак. В шаблоне шаблонного метода шаблон не относится к этой общей концепции программирования. Он просто означает, что у нас будет шаблон для нашего алгоритма.

class BaseCar {
public:
    void fuelUpCar() {
        stopTheCar();
        plugTheFeed();
        waitUntilEnoughFuelTransmitted();
        unplugTheFeed();
    }

    // ...
};
Вход в полноэкранный режим Выход из полноэкранного режима

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

class BaseCar {
public:
    void fuelUpCar() {
        stopTheCar();
        plugTheFeed();
        waitUntilEnoughFuelTransmitted();
        unplugTheFeed();
    }

private:
    virtual void stopTheCar() { /* ... */ };
    virtual void plugTheFeed() = 0;
    virtual void waitUntilEnoughFuelTransmitted() { /* ... */ };
    virtual void unplugTheFeed() = 0;

    // ...
};
Вход в полноэкранный режим Выход из полноэкранного режима

Использование TMP имеет несколько преимуществ.

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

Идиома невиртуального интерфейса

Пришло время обсудить идиому невиртуального интерфейса.

Вы могли заметить, что виртуальные функции, которые мы создали, перечислены после спецификатора доступа private. Разработка программного обеспечения — это разрушение сложностей. Программирование — это упрощение сложного. Вспомните первый принцип SOLID. Сущность должна отвечать за одну вещь, не более. Или, в лучшей интерпретации, мы бы сказали, что сущность должна меняться только по одной единственной причине. Тем не менее, первая интерпретация показывает присущее нам стремление к простоте.

Невиртуальные интерфейсы — это простота. Давайте подумаем, что представляют собой публичные виртуальные функции?!

Они представляют собой как точку настройки для реализации, так и общедоступный интерфейс.

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

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

class BaseCar {
public:
    void fuelUpCar() {
        stopTheCar();
        plugTheFeed();
        waitUntilEnoughFuelTransmitted();
        unplugTheFeed();
    }

private:
    virtual void stopTheCar() { /* ... */ };
    virtual void plugTheFeed() = 0;
    virtual void waitUntilEnoughFuelTransmitted() { /* ... */ };
    virtual void unplugTheFeed() = 0;

    // ...
};

class ElectricCar : public BaseCar {
private:
    void plugTheFeed() override { /* ... */}
    void unplugTheFeed() override { /* ... */}
};

class FossilFuelCar : public BaseCar {
private:
    void plugTheFeed() override { /* ... */}
    void unplugTheFeed() override { /* ... */}
};
Вход в полноэкранный режим Выход из полноэкранного режима

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

Однако есть один публичный метод, который все же должен быть виртуальным. Деструктор. Мы, наверное, все знаем, что удаление полиморфного объекта, удаление производного класса через указатель базового класса без виртуального деструктора приводит к неопределенному поведению.

BaseCar* car = new ElectricCar{};
delete car; // this is UB!
Вход в полноэкранный режим Выход из полноэкранного режима

Если вы не удаляете объекты подобным образом, то бояться нечего. Проблема в том, что вы не можете делать такие предположения, даже если вы избегаете удаления через указатели базового класса, вы не можете быть уверены, что кто-то не придет и не сделает это. И иногда это будет весьма ограничивающим фактором. Лучше перестраховаться, деструктор не является частью идиомы NVI, и мы должны сделать наши деструкторы базовых классов виртуальными.

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

Заключение

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

Подключайтесь глубже

Если вам понравилась эта статья, пожалуйста

  • нажмите на кнопку «Мне нравится»,
  • подпишитесь на мою рассылку
  • и давайте общаться в Twitter!

Оцените статью
devanswers.ru
Добавить комментарий