Паттерн Декоратор (Decorator) представляет собой немного спорный паттерн. Его почти всегда можно заменить чем-нибудь другим. Например, Компоновщиком или Строителем.
Но все же Декоратор вполне может найти свое место в вашем проекте. Не зря его активно используют при разработке библиотек ввода-вывода. Особенно это заметно в Java.
В основе паттерна Декоратор лежит базовый интерфейс (или абстрактный класс). Для него создаются реализации, выполняющие полезные действия. Декоратор при этом выступает в качестве прозрачной обертки над реализациями, имея тот же самый интерфейс. Он делегирует все вызовы оборачиваемому объекту, но добавляет свои специфические преобразования.
Преимущество от использования Декоратора заключается в том, что он позволяет комбинировать цепочки обернутых объектов произвольной сложности. В результате удается избежать ненужного дублирования кода.
Рассмотрим пример. Создадим небольшое приложение, которое применяет графические фильтры к загруженному изображению. Фильтры могут накладываться, поэтому Декоратор позволит нам динамически создавать комбинированные фильтры, доступные для повторного использования.
Интерфейс графического фильтра
Спроектируем простой интерфейсный класс ImageFilter. Заголовочный файл imagefilter.h:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
#ifndef IMAGEFILTER_H #define IMAGEFILTER_H #include <QPixmap> class ImageFilter { public: ImageFilter(); virtual ~ImageFilter(); virtual QPixmap apply( const QPixmap& pix ) = 0; }; #endif // IMAGEFILTER_H |
Он имеет всего одну чисто виртуальную функцию apply(), которая принимает на вход QPixmap, и возвращает результат применения фильтра.
Поскольку нам интересны как фильтры по отдельности, так и комбинации фильтров, то каждая реализация станет потенциальным Декоратором. Начнем с фильтра отзеркаливания.
Фильтр отзеркаливания изображения
В Qt довольно легко создать подобный эффект. Объявим класс MirroredImageFilter, наследующий ImageFilter, в mirroredimagefilter.h:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
#ifndef MIRROREDIMAGEFILTER_H #define MIRROREDIMAGEFILTER_H #include <imagefilter.h> #include <memory> class MirroredImageFilter : public ImageFilter { public: explicit MirroredImageFilter( const std::shared_ptr< ImageFilter >& filter = nullptr ); QPixmap apply( const QPixmap& pix ); private: std::shared_ptr< ImageFilter > m_filter; }; #endif // MIRROREDIMAGEFILTER_H |
В качестве параметра конструктор принимает указатель на оборачиваемый фильтр. Если параметр равен nullptr, то класс ничего не декорирует, и выступает в качестве самостоятельной реализации. А вот и она, в файле mirroredimagefilter.cpp:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
#include "mirroredimagefilter.h" MirroredImageFilter::MirroredImageFilter( const std::shared_ptr< ImageFilter >& filter ) : m_filter( filter ) { } QPixmap MirroredImageFilter::apply( const QPixmap& pix ) { QPixmap pixResult = pix; if( m_filter ) { pixResult = m_filter->apply( pix ); } return QPixmap::fromImage( pixResult.toImage().mirrored( true, false ) ); } |
Пробный запуск
Один фильтр уже есть. Проверим его работу. Файл main.cpp:
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 |
#include <QApplication> #include <QFileDialog> #include <QLabel> #include "mirroredimagefilter.h" int main( int argc, char** argv ) { QApplication app( argc, argv ); QString fileName = QFileDialog::getOpenFileName( nullptr, "Load Image", QString(), "Images (*.jpg *.png *.bmp)" ); QPixmap pix; if( pix.load( fileName ) ) { QLabel lblMirrored; lblMirrored.setPixmap( MirroredImageFilter().apply( pix ) ); lblMirrored.show(); return app.exec(); } return 0; } |
Эта программа уже что-то делает, но пока что Декоратора здесь еще нет. Он появится, когда мы добавим хотя бы еще один фильтр. Займемся этим прямо сейчас.
Фильтр поворота изображения
Пусть второй фильтр-Декоратор умеет осуществлять поворот изображения на указанный угол. Посмотрим на соответствующий заголовочный файл rotatedimagefilter.h:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
#ifndef ROTATEDIMAGEFILTER_H #define ROTATEDIMAGEFILTER_H #include "imagefilter.h" #include <memory> class RotatedImageFilter : public ImageFilter { public: RotatedImageFilter( double angle = 90.0, const std::shared_ptr< ImageFilter >& filter = nullptr ); QPixmap apply( const QPixmap& pix ); private: double m_angle; std::shared_ptr< ImageFilter > m_filter; }; #endif // ROTATEDIMAGEFILTER_H |
А вот и реализация в rotatedimagefilter.cpp:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
#include "rotatedimagefilter.h" RotatedImageFilter::RotatedImageFilter( double angle, const std::shared_ptr< ImageFilter >& filter ) : m_angle( angle ), m_filter( filter ) { } QPixmap RotatedImageFilter::apply( const QPixmap& pix ) { QPixmap pixResult = pix; if( m_filter ) { pixResult = m_filter->apply( pix ); } return pixResult.transformed( QTransform().rotate( m_angle ) ); } |
Все вместе
Теперь у нас есть два фильтра. Каждый из них можно использовать отдельно, а можно скомбинировать в цепочку. Попробуем все это на практике (файл main.cpp):
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 |
#include <QApplication> #include <QFileDialog> #include <QLabel> #include "mirroredimagefilter.h" #include "rotatedimagefilter.h" int main( int argc, char** argv ) { QApplication app( argc, argv ); QString fileName = QFileDialog::getOpenFileName( nullptr, "Load Image", QString(), "Images (*.jpg *.png *.bmp)" ); QPixmap pix; if( pix.load( fileName ) ) { QLabel lblMirrored; lblMirrored.setPixmap( MirroredImageFilter().apply( pix ) ); lblMirrored.show(); QLabel lblRotated; lblRotated.setPixmap( RotatedImageFilter().apply( pix ) ); lblRotated.show(); QLabel lblCombined; lblCombined.setPixmap( RotatedImageFilter( 45.0, std::make_shared< MirroredImageFilter >() ).apply( pix ) ); lblCombined.show(); return app.exec(); } return 0; } |
В результате запуска программы на экране появятся три QLabel: с отзеркаленным изображением, с повернутым на 90 градусов изображением и с изображением, которое было сначала отзеркалено, а потом повернуто на 45 градусов.
Выводы
Поскольку мы создали всего два фильтра, то существенную выгоду от использования паттерна Декоратор увидеть сложно. Но с ростом числа реализаций количество их возможных комбинаций будет возрастать очень быстро. В этом случае Декоратор в сочетании со Стратегией или каким-нибудь другим полиморфным подходом окажется весьма привлекательным решением.