C++ Decorator Pattern: Complete Guide | Dynamic Wrapping & Composition

C++ Decorator Pattern: Complete Guide | Dynamic Wrapping & Composition

이 글의 핵심

Decorator pattern in C++: wrap components to add behavior at runtime—streams, logging, text formatting, and builder-style composition.

See structural patterns in context: C++ structural patterns #19-2 and design patterns overview #20-2. Python’s decorators and JavaScript patterns show similar “wrap behavior” ideas with different syntax.

What is Decorator Pattern? why you need it

Problem Scenario: Feature Combination Explosion

Problem: If I want to add milk, sugar, and whipped cream to my coffee, I need to make all combinations a class.

// Bad example: class explosion
class Coffee {};
class CoffeeWithMilk : public Coffee {};
class CoffeeWithSugar : public Coffee {};
class CoffeeWithMilkAndSugar : public Coffee {};
class CoffeeWithMilkAndSugarAndWhip : public Coffee {};
// As the combination increases, the class explodes

Solution: Decorator Pattern adds features dynamically. A Decorator wraps a Component and adds functionality.

// Good example: Decorator
auto coffee = std::make_unique<SimpleCoffee>();
coffee = std::make_unique<MilkDecorator>(std::move(coffee));
coffee = std::make_unique<SugarDecorator>(std::move(coffee));
// function combination at runtime
flowchart TD
    component["Component (Coffee)"]
    simple["SimpleCoffee"]
    decorator["Decorator"]
    milk["MilkDecorator"]
    sugar["SugarDecorator"]
    
    component <|-- simple
    component <|-- decorator
    decorator <|-- milk
    decorator <|-- sugar
    decorator --> component

index

  1. Basic structure
  2. Stream Decorator
  3. Logging system
  4. Frequently occurring problems and solutions
  5. Production Patterns
  6. Complete example: Text formatter

1. basic structure

#include <iostream>
#include <memory>
#include <string>

class Coffee {
public:
    virtual std::string getDescription() const = 0;
    virtual double cost() const = 0;
    virtual ~Coffee() = default;
};

class SimpleCoffee : public Coffee {
public:
    std::string getDescription() const override {
        return "Simple coffee";
    }
    
    double cost() const override {
        return 2.0;
    }
};

class CoffeeDecorator : public Coffee {
public:
    CoffeeDecorator(std::unique_ptr<Coffee> c)
        : coffee(std::move(c)) {}
    
protected:
    std::unique_ptr<Coffee> coffee;
};

class MilkDecorator : public CoffeeDecorator {
public:
    using CoffeeDecorator::CoffeeDecorator;
    
    std::string getDescription() const override {
        return coffee->getDescription() + " + Milk";
    }
    
    double cost() const override {
        return coffee->cost() + 0.5;
    }
};

class SugarDecorator : public CoffeeDecorator {
public:
    using CoffeeDecorator::CoffeeDecorator;
    
    std::string getDescription() const override {
        return coffee->getDescription() + " + Sugar";
    }
    
    double cost() const override {
        return coffee->cost() + 0.3;
    }
};

int main() {
    auto coffee = std::make_unique<SimpleCoffee>();
    std::cout << coffee->getDescription() << ": $" << coffee->cost() << '\n';
    
    coffee = std::make_unique<MilkDecorator>(std::move(coffee));
    std::cout << coffee->getDescription() << ": $" << coffee->cost() << '\n';
    
    coffee = std::make_unique<SugarDecorator>(std::move(coffee));
    std::cout << coffee->getDescription() << ": $" << coffee->cost() << '\n';
}

output of power:

Simple coffee: $2
Simple coffee + Milk: $2.5
Simple coffee + Milk + Sugar: $2.8

2. stream decorator

#include <iostream>
#include <memory>
#include <string>
#include <algorithm>

class DataStream {
public:
    virtual void write(const std::string& data) = 0;
    virtual std::string read() = 0;
    virtual ~DataStream() = default;
};

class FileStream : public DataStream {
public:
    void write(const std::string& data) override {
        buffer = data;
        std::cout << "[File] Written: " << data << '\n';
    }
    
    std::string read() override {
        std::cout << "[File] Reading\n";
        return buffer;
    }
    
private:
    std::string buffer;
};

class StreamDecorator : public DataStream {
public:
    StreamDecorator(std::unique_ptr<DataStream> s)
        : stream(std::move(s)) {}
    
protected:
    std::unique_ptr<DataStream> stream;
};

class EncryptionDecorator : public StreamDecorator {
public:
    using StreamDecorator::StreamDecorator;
    
    void write(const std::string& data) override {
        std::string encrypted = encrypt(data);
        std::cout << "[Encryption] Encrypting\n";
        stream->write(encrypted);
    }
    
    std::string read() override {
        std::string encrypted = stream->read();
        std::cout << "[Encryption] Decrypting\n";
        return decrypt(encrypted);
    }
    
private:
    std::string encrypt(const std::string& data) {
        std::string result = data;
        std::reverse(result.begin(), result.end());
        return result;
    }
    
    std::string decrypt(const std::string& data) {
return encrypt(data);  // symmetry
    }
};

class CompressionDecorator : public StreamDecorator {
public:
    using StreamDecorator::StreamDecorator;
    
    void write(const std::string& data) override {
        std::string compressed = compress(data);
        std::cout << "[Compression] Compressing\n";
        stream->write(compressed);
    }
    
    std::string read() override {
        std::string compressed = stream->read();
        std::cout << "[Compression] Decompressing\n";
        return decompress(compressed);
    }
    
private:
    std::string compress(const std::string& data) {
        return "[COMPRESSED]" + data;
    }
    
    std::string decompress(const std::string& data) {
return data.substr(12);  // remove "[COMPRESSED]"
    }
};

int main() {
    auto stream = std::make_unique<FileStream>();
    stream = std::make_unique<EncryptionDecorator>(std::move(stream));
    stream = std::make_unique<CompressionDecorator>(std::move(stream));
    
    stream->write("Hello, World!");
    std::string data = stream->read();
    std::cout << "Result: " << data << '\n';
}

3. logging system

#include <iostream>
#include <memory>
#include <chrono>
#include <iomanip>

class Logger {
public:
    virtual void log(const std::string& message) = 0;
    virtual ~Logger() = default;
};

class ConsoleLogger : public Logger {
public:
    void log(const std::string& message) override {
        std::cout << message << '\n';
    }
};

class LoggerDecorator : public Logger {
public:
    LoggerDecorator(std::unique_ptr<Logger> l)
        : logger(std::move(l)) {}
    
protected:
    std::unique_ptr<Logger> logger;
};

class TimestampDecorator : public LoggerDecorator {
public:
    using LoggerDecorator::LoggerDecorator;
    
    void log(const std::string& message) override {
        auto now = std::chrono::system_clock::now();
        auto time = std::chrono::system_clock::to_time_t(now);
        std::cout << "[" << std::put_time(std::localtime(&time), "%Y-%m-%d %H:%M:%S") << "] ";
        logger->log(message);
    }
};

class LevelDecorator : public LoggerDecorator {
public:
    LevelDecorator(std::unique_ptr<Logger> l, const std::string& level)
        : LoggerDecorator(std::move(l)), level_(level) {}
    
    void log(const std::string& message) override {
        logger->log("[" + level_ + "] " + message);
    }
    
private:
    std::string level_;
};

int main() {
    auto logger = std::make_unique<ConsoleLogger>();
    logger = std::make_unique<TimestampDecorator>(std::move(logger));
    logger = std::make_unique<LevelDecorator>(std::move(logger), "INFO");
    
    logger->log("Application started");
}

4. Frequently occurring problems and solutions

Problem 1: Type loss

Symptom: Original type information is lost when wrapped with Decorator.

// ❌ Incorrect use
SimpleCoffee* simple = new SimpleCoffee();
Coffee* decorated = new MilkDecorator(simple);
// Cannot call specific method of simple

// ✅ Solved: dynamic_cast when needed
if (auto* simple = dynamic_cast<SimpleCoffee*>(decorated)) {
    simple->specificMethod();
}

Problem 2: Order dependency

Symptom: Results vary depending on the decorator order.

// Encryption -> Compression vs Compression -> Encryption
// Results may vary

5. production pattern

Pattern 1: Builder Style

class CoffeeBuilder {
    std::unique_ptr<Coffee> coffee;
public:
    CoffeeBuilder() : coffee(std::make_unique<SimpleCoffee>()) {}
    
    CoffeeBuilder& addMilk() {
        coffee = std::make_unique<MilkDecorator>(std::move(coffee));
        return *this;
    }
    
    CoffeeBuilder& addSugar() {
        coffee = std::make_unique<SugarDecorator>(std::move(coffee));
        return *this;
    }
    
    std::unique_ptr<Coffee> build() {
        return std::move(coffee);
    }
};

auto coffee = CoffeeBuilder()
    .addMilk()
    .addSugar()
    .build();

6. Complete example: text formatter

#include <iostream>
#include <memory>
#include <string>
#include <algorithm>

class TextFormatter {
public:
    virtual std::string format(const std::string& text) = 0;
    virtual ~TextFormatter() = default;
};

class PlainTextFormatter : public TextFormatter {
public:
    std::string format(const std::string& text) override {
        return text;
    }
};

class FormatterDecorator : public TextFormatter {
public:
    FormatterDecorator(std::unique_ptr<TextFormatter> f)
        : formatter(std::move(f)) {}
    
protected:
    std::unique_ptr<TextFormatter> formatter;
};

class BoldDecorator : public FormatterDecorator {
public:
    using FormatterDecorator::FormatterDecorator;
    
    std::string format(const std::string& text) override {
        return "<b>" + formatter->format(text) + "</b>";
    }
};

class ItalicDecorator : public FormatterDecorator {
public:
    using FormatterDecorator::FormatterDecorator;
    
    std::string format(const std::string& text) override {
        return "<i>" + formatter->format(text) + "</i>";
    }
};

class UpperCaseDecorator : public FormatterDecorator {
public:
    using FormatterDecorator::FormatterDecorator;
    
    std::string format(const std::string& text) override {
        std::string result = formatter->format(text);
        std::transform(result.begin(), result.end(), result.begin(), ::toupper);
        return result;
    }
};

int main() {
    auto formatter = std::make_unique<PlainTextFormatter>();
    formatter = std::make_unique<BoldDecorator>(std::move(formatter));
    formatter = std::make_unique<ItalicDecorator>(std::move(formatter));
    formatter = std::make_unique<UpperCaseDecorator>(std::move(formatter));
    
    std::cout << formatter->format("Hello, World!") << '\n';
    // <I><B>HELLO, WORLD!</B></I>
}

organize

conceptDescription
Decorator PatternAdd features dynamically
PurposeExtending functionality without inheritance
StructureComponent, ConcreteComponent, Decorator
AdvantagesCombination Flexible, OCP Compliant, Runtime Additions
DisadvantagesType loss, order dependence, increased complexity
Use CaseStreams, logging, UI, text formatting

The Decorator Pattern is a powerful pattern that dynamically combines functionality.


FAQ

Q1: When do I use Decorator Pattern?

A: Used when functions are added dynamically and there are too many combinations and it is difficult to solve through inheritance.

Q2: Inheritance vs Decorator?

A: Inheritance can be combined statically, and Decorator can be combined dynamically.

Q3: What is the difference from Adapter?

A: Adapter focuses on interface conversion, Decorator focuses on feature addition.

Q4: What is the performance overhead?

A: Longer Decorator chains increase indirection.

Q5: What about type loss issues?

A: Restore the original type with dynamic_cast, or use the Visitor Pattern.

Q6: What are the Decorator Pattern learning resources?

A:

One-line summary: The Decorator Pattern allows you to dynamically combine features. Next, it would be a good idea to read Adapter Pattern.


Good article to read together (internal link)

Here’s another article related to this topic.

  • C++ Adapter Pattern Complete Guide | Interface conversion and compatibility
  • C++ Proxy Pattern Complete Guide | Access control and lazy loading

Practical tips

These are tips that can be applied right away in practice.

Debugging tips

  • If you run into a problem, check the compiler warnings first.
  • Reproduce the problem with a simple test case

Performance Tips

  • Don’t optimize without profiling
  • Set measurable indicators first

Code review tips

  • Check in advance for areas that are frequently pointed out in code reviews.
  • Follow your team’s coding conventions

Practical checklist

This is what you need to check when applying this concept in practice.

Before writing code

  • Is this technique the best way to solve the current problem?
  • Can team members understand and maintain this code?
  • Does it meet the performance requirements?

Writing code

  • Have you resolved all compiler warnings?
  • Have you considered edge cases?
  • Is error handling appropriate?

When reviewing code

  • Is the intent of the code clear?
  • Are there enough test cases?
  • Is it documented?

Use this checklist to reduce mistakes and improve code quality.


Keywords covered in this article (related search terms)

This article will be helpful if you search for C++, decorator, pattern, wrapper, composition, inheritance, etc.


  • C++ Mixin |