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

/! \: Эта статья изначально была опубликована в моем блоге. Если вы заинтересованы в получении моих последних статей, подпишитесь на мою рассылку.

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

  1. Адаптер
  2. Мост
  3. Составное
  4. Декоратор
  5. Фасад
  6. Легковес
  7. Прокси

Фрагменты кода, которые вы видите в этой серии статей, упрощены, а не сложны. Поэтому вы часто видите, что я не использую такие ключевые слова, как override, final, public (при наследовании), просто чтобы сделать код компактным и потребляемым (большую часть времени) на одном стандартном размере экрана. Я также предпочитаю struct вместо class, просто чтобы сохранить строку, не записывая иногда public:, а также пропускаю виртуальный деструктор, конструктор, конструктор копирования, префикс std::, намеренно удаляя динамическую память. Я также считаю себя прагматичным человеком, который хочет передать идею максимально простым способом, а не стандартным способом или с использованием жаргонов.

Примечание.

  • Если вы случайно наткнулись здесь, то я бы посоветовал вам пройти через Что такое шаблон проектирования? во-первых, даже если это банально. Я считаю, что это побудит вас изучить эту тему больше.
  • Весь этот код, с которым вы сталкиваетесь в этой серии статей, скомпилирован с использованием C ++ 20 (хотя в большинстве случаев я использовал функции Modern C ++ вплоть до C ++ 17). Поэтому, если у вас нет доступа к последней версии компилятора, вы можете использовать https://wandbox.org/, в котором также есть предустановленная библиотека ускорения.

Намерение

Чтобы облегчить дополнительные функции для объектов.

Примеры шаблонов проектирования декораторов в C ++

  • И для этого у нас есть два разных варианта шаблона проектирования декоратора в C ++:
  1. Динамический декоратор. Агрегируйте декорированный объект по ссылке или указателю.
  2. Статический декоратор: наследуйте от декорированного объекта.

Динамический декоратор

struct Shape {
    virtual operator string() = 0;
};
struct Circle : Shape {
    float   m_radius;
    Circle(const float radius = 0) : m_radius{radius} {}
    void resize(float factor) { m_radius *= factor; }
    operator string() {
        ostringstream oss;
        oss << "A circle of radius " << m_radius;
        return oss.str();
    }
};
struct Square : Shape {
    float   m_side;
    Square(const float side = 0) : m_side{side} {}
    operator string() {
        ostringstream oss;
        oss << "A square of side " << m_side;
        return oss.str();
    }
};
  • Итак, у нас есть иерархия из двух разных Shape (то есть Square и Circle), и мы хотим улучшить эту иерархию, добавив к ней цвет. Теперь мы внезапно не собираемся создавать два других класса, например. цветной круг и цветной квадрат. Это было бы слишком и не масштабируемый вариант.
  • Скорее, мы можем просто получить ColoredShape следующим образом.
struct ColoredShape : Shape {
    const Shape&    m_shape;
    string          m_color;
    ColoredShape(const Shape &s, const string &c) : m_shape{s}, m_color{c} {}
    operator string() {
        ostringstream oss;
        oss << string(const_cast<Shape&>(m_shape)) << " has the color " << m_color;
        return oss.str();
    }
};
// we are not changing the base class of existing objects
// cannot make, e.g., ColoredSquare, ColoredCircle, etc.
int main() {
    Square square{5};
    ColoredShape green_square{square, "green"};    
    cout << string(square) << endl << string(green_square) << endl;
    // green_circle.resize(2); // Not available
    return EXIT_SUCCESS;
}

Почему это динамический декоратор?
Потому что вы можете создать экземпляр ColoredShape во время выполнения, предоставив необходимые аргументы. Другими словами, вы можете решить во время выполнения, какие Shape (т.е. Circle или Square) будут окрашены.

  • Вы даже можете смешивать декораторы следующим образом:
struct TransparentShape : Shape {
    const Shape&    m_shape;
    uint8_t         m_transparency;
    TransparentShape(const Shape& s, const uint8_t t) : m_shape{s}, m_transparency{t} {}
    operator string() {
        ostringstream oss;
        oss << string(const_cast<Shape&>(m_shape)) << " has "
            << static_cast<float>(m_transparency) / 255.f * 100.f
            << "% transparency";
        return oss.str();
    }
};
int main() {
    TransparentShape TransparentShape{ColoredShape{Square{5}, "green"}, 51};
    cout << string(TransparentShape) << endl;
    return EXIT_SUCCESS;
}

Ограничение динамического декоратора

Если вы посмотрите на определение Circle, вы увидите, что у круга есть метод, называемый resize(). мы не можем использовать этот метод, поскольку мы сделали агрегирование на основе интерфейса Shape &, связанного с единственным методом, представленным в нем.

Статический декоратор

  • Динамический декоратор хорош, если вы не знаете, какой объект собираетесь украсить, и хотите иметь возможность выбирать их во время выполнения, но иногда вы знаете декоратор, который хотите, во время компиляции, и в этом случае вы можете использовать комбинацию Шаблоны C ++ и наследование.
template <class T>  // Note: `class`, not typename
struct ColoredShape : T {
    static_assert(is_base_of<Shape, T>::value, "Invalid template argument"); // Compile time safety
    string      m_color;
    template <typename... Args>
    ColoredShape(const string &c, Args &&... args) : m_color(c), T(std::forward<Args>(args)...) { }
    operator string() {
        ostringstream oss;
        oss << T::operator string() << " has the color " << m_color;
        return oss.str();
    }
};
template <typename T>
struct TransparentShape : T {
    uint8_t     m_transparency;
    template <typename... Args>
    TransparentShape(const uint8_t t, Args... args) : m_transparency{t}, T(std::forward<Args>(args)...) { }
    operator string() {
        ostringstream oss;
        oss << T::operator string() << " has "
            << static_cast<float>(m_transparency) / 255.f * 100.f
            << "% transparency";
        return oss.str();
    }
};
int main() {
    ColoredShape<Circle> green_circle{"green", 5};
    green_circle.resize(2);
    cout << string(green_circle) << endl;
    // Mixing decorators
    TransparentShape<ColoredShape<Circle>> green_trans_circle{51, "green", 5};
    green_trans_circle.resize(2);
    cout << string(green_trans_circle) << endl;
    return EXIT_SUCCESS;
}
  • Как видите, теперь мы можем вызвать метод resize(), который был ограничением динамического декоратора. Вы даже можете смешивать декораторы, как мы это делали ранее.
  • По сути, этот пример демонстрирует, что если вы готовы отказаться от динамической композиции декоратора и если вы готовы определить все декораторы во время компиляции, вы получите дополнительное преимущество от использования наследования.
  • И таким образом вы фактически получаете доступ к элементам любого объекта, который вы украшаете, через декоратор и смешанный декоратор.

Функциональный подход к шаблону проектирования декоратора с использованием современного C ++

  • До сих пор мы говорили о шаблоне дизайна Decorator, который украшает класс, но вы можете сделать то же самое для функций. Ниже приведен типичный пример того же регистратора:
// Need partial specialization for this to work
template <typename T>
struct Logger;
// Return type and argument list
template <typename R, typename... Args>
struct Logger<R(Args...)> {
    function<R(Args...)>    m_func;
    string                  m_name;
    Logger(function<R(Args...)> f, const string &n) : m_func{f}, m_name{n} { }
    R operator()(Args... args) {
        cout << "Entering " << m_name << endl;
        R result = m_func(args...);
        cout << "Exiting " << m_name << endl;
        return result;
    }
};
template <typename R, typename... Args>
auto make_logger(R (*func)(Args...), const string &name) {
    return Logger<R(Args...)>(std::function<R(Args...)>(func), name);
}
double add(double a, double b) { return a + b; }
int main() {
    auto logged_add = make_logger(add, "Add");
    auto result = logged_add(2, 3);
    return EXIT_SUCCESS;
}
  • Приведенный выше пример может показаться вам немного сложным, но если у вас есть четкое представление о вариативном храме, вам не понадобится более 30 секунд, чтобы понять, что здесь происходит.

Преимущества шаблона дизайна Decorator

  1. Декоратор упрощает расширение функциональности существующего объекта во время выполнения и компиляции.
  2. Decorator также обеспечивает гибкость для добавления любого количества декораторов в любом порядке и смешивания.
  3. Декораторы - хорошее решение проблем с перестановкой, потому что вы можете обернуть компонент любым количеством декораторов.
  4. Это разумный выбор - применить шаблон дизайна Decorator для уже отправленного кода. Потому что это обеспечивает обратную совместимость приложения и меньше тестирования на уровне модулей, поскольку изменения не влияют на другие части кода.

Резюме по часто задаваемым вопросам

Когда использовать шаблон дизайна "Декоратор"?

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

Каковы недостатки использования шаблона дизайна Decorator?

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

В чем разница между шаблоном проектирования адаптера и декоратора?

- Адаптер изменяет интерфейс существующего объекта
- Декоратор улучшает интерфейс существующего объекта.

В чем разница между шаблоном проектирования прокси и декоратором?

- Прокси предоставляет аналогичный или простой интерфейс
- Декоратор предоставляет расширенный интерфейс