Aplicando o Princípio da Responsabilidade Única: Padrões Decorator e Bridge em C++

O Problema da Herança Excessiva e o Princípio da Responsabilidade Única

No design de componentes de software, a má divisão de responsabilidades frequentemente leva a um problema clássico: o uso indiscriminado de herança para adicionar funcionalidades. À medida que os requisitos evoluem, essa abordagem resulta em uma hierarquia de subclasses inchada, repleta de código duplicado e extremamente rígida. A solução para mitigar esse problema reside na aplicação rigorosa do Princípio da Responsabilidade Única (SRP), utilizando padrões de projeto estruturais como Decorator e Bridge para划清 (delimitar) responsabilidades.

Padrão Decorator (Decorador)

Motivação e Definição

Em certas situações, desenvolvedores tendem a usar herança de forma excessiva para estender o comportamento de objetos. Como a herança introduz características estáticas em tempo de compilação, essa abordagem carece de flexibilidade. Além disso, o aumento de funcionalidades gera uma explosão combinatória de subclasses.

O padrão Decorator resolve isso permitindo que novas responsabilidades sejam adicionadas a um objeto dinamicamente. Em termos de flexibilidade para adicionar funções, a composição utilizada pelo Decorator é muito superior à geração de subclasses, eliminando código redundante e reduzindo drasticamente o número de classes.

Implementação em C++

Considere um sistema de processamento de dados onde precisamos de diferentes canais de comunicação (arquivo, rede, memória) e a possibilidade de adicionar funcionalidades transversais como criptografia e cache.

#include <iostream>
#include <string>

// Componente base (Abstração do canal)
class DataChannel {
public:
    virtual void transmit(const std::string& payload) = 0;
    virtual std::string receive() = 0;
    virtual ~DataChannel() = default;
};

// Componentes Concretos (Implementações base)
class FileChannel : public DataChannel {
public:
    void transmit(const std::string& payload) override { /* Lógica de escrita em arquivo */ }
    std::string receive() override { return "file_data"; }
};

class NetworkChannel : public DataChannel {
public:
    void transmit(const std::string& payload) override { /* Lógica de envio de rede */ }
    std::string receive() override { return "network_data"; }
};

// Decorator Base (Mantém a referência para o componente)
class ChannelDecorator : public DataChannel {
protected:
    DataChannel* channel;
public:
    ChannelDecorator(DataChannel* ch) : channel(ch) {}
    virtual ~ChannelDecorator() { delete channel; }
};

// Decorator Concreto 1: Criptografia
class SecureChannel : public ChannelDecorator {
public:
    SecureChannel(DataChannel* ch) : ChannelDecorator(ch) {}
    
    void transmit(const std::string& payload) override {
        std::string encrypted = "ENC(" + payload + ")";
        channel->transmit(encrypted);
    }
    
    std::string receive() override {
        std::string raw = channel->receive();
        return "DEC(" + raw + ")";
    }
};

// Decorator Concreto 2: Cache/Buffer
class CachedChannel : public ChannelDecorator {
public:
    CachedChannel(DataChannel* ch) : ChannelDecorator(ch) {}
    
    void transmit(const std::string& payload) override {
        // Lógica de armazenamento em cache antes do envio
        channel->transmit(payload);
    }
    
    std::string receive() override {
        // Lógica de leitura do cache
        return channel->receive();
    }
};

// Montagem em tempo de execução
void setupPipeline() {
    DataChannel* base = new FileChannel();
    DataChannel* secured = new SecureChannel(base);
    DataChannel* optimized = new CachedChannel(secured);
    
    optimized->transmit("dados_sensiveis");
    delete optimized; // A deleção em cascata limpa a cadeia
}

Pontos-Chave do Decorator

  • Ao utilizar composição em vez de herança, o padrão permite a extensão dinâmica de funcionalidades em tempo de execução, evitando a rigidez e a proliferação de subclasses.
  • Na interface, o Decorator manifesta uma relação is-a com o componente base (herda a intreface). Na implementação, manifesta uma relação has-a (compõe outro componente).
  • O objetivo não é resolver herança múltipla, mas sim lidar com a expansão de funcionalidades em múltiplas direções ortogonais (o verdadeiro significado de "decorar").
  • Regra prática: Se a classe A herda da classe B e também mantém uma instância (compõe) da classe B, há uma probabilidade muito alta de ser uma aplicação do padrão Decorator.

Padrão Bridge (Ponte)

Motivação e Definição

Devido à lógica de implementação inerente a certos tipos, eles podem possuir dois ou mais dimensões de variação independentes. Como lidar com essa "multidimensionalidade" sem introduzir complexidade excessiva?

O padrão Bridge define que a abstração (lógica de negócio) e sua implementação (mecanismo ou plataforma) devem ser desacopladas, permitindo que ambas evoluam e variem de forma independente.

Implementação em C++: Sistema de Notificações

Neste exemplo, separamos a lógica de envio de notificações (Abstração) da renderização específica da plataforma (Implementação).

#include <iostream>
#include <string>

// Implementador (Interface da Plataforma)
class RenderingPlatform {
public:
    virtual void drawText(const std::string& text) = 0;
    virtual void drawImage(const std::string& img) = 0;
    virtual void playSound() = 0;
    virtual ~RenderingPlatform() = default;
};

// Implementadores Concretos
class WindowsPlatform : public RenderingPlatform {
public:
    void drawText(const std::string& text) override { /* API Win32 para texto */ }
    void drawImage(const std::string& img) override { /* API Win32 para imagem */ }
    void playSound() override { /* API Win32 para áudio */ }
};

class LinuxPlatform : public RenderingPlatform {
public:
    void drawText(const std::string& text) override { /* X11/Wayland para texto */ }
    void drawImage(const std::string& img) override { /* X11/Wayland para imagem */ }
    void playSound() override { /* ALSA/PulseAudio */ }
};

// Abstração (Lógica de Negócio)
class NotificationService {
protected:
    RenderingPlatform* platform;
public:
    NotificationService(RenderingPlatform* plat) : platform(plat) {}
    virtual void notifyText(const std::string& msg) = 0;
    virtual void notifyMedia(const std::string& media) = 0;
    virtual ~NotificationService() { delete platform; }
};

// Abstrações Refinadas
class SilentNotification : public NotificationService {
public:
    SilentNotification(RenderingPlatform* plat) : NotificationService(plat) {}
    void notifyText(const std::string& msg) override { platform->drawText(msg); }
    void notifyMedia(const std::string& media) override { platform->drawImage(media); }
};

class AlertNotification : public NotificationService {
public:
    AlertNotification(RenderingPlatform* plat) : NotificationService(plat) {}
    void notifyText(const std::string& msg) override {
        platform->playSound();
        platform->drawText(msg);
    }
    void notifyMedia(const std::string& media) override {
        platform->playSound();
        platform->drawImage(media);
    }
};

// Montagem em tempo de execução
void configureSystem() {
    NotificationService* service = new AlertNotification(new LinuxPlatform());
    service->notifyText("Atualização do Sistema");
    delete service;
}

Implementação em C++: Dispositivos Inteligentes e Aplicativos

Outra perspectiva do Bridge é a combinação de hardware (marca/tipo) com software (funcionalidade).

#include <iostream>

// Abstração de Software
class Application {
public:
    virtual void execute() = 0;
    virtual ~Application() = default;
};

class FitnessTracker : public Application {
public:
    void execute() override { std::cout << "Monitorando batimentos e passos.\n"; }
};

class MusicPlayer : public Application {
public:
    void execute() override { std::cout << "Reproduzindo playlist.\n"; }
};

// Abstração de Hardware (Ponte)
class SmartDevice {
protected:
    Application* app;
public:
    void installApp(Application* a) { this->app = a; }
    virtual void runApp() = 0;
    virtual ~SmartDevice() { delete app; }
};

class Smartwatch : public SmartDevice {
public:
    void runApp() override {
        std::cout << "[Smartwatch] ";
        app->execute();
    }
};

class SmartSpeaker : public SmartDevice {
public:
    void runApp() override {
        std::cout << "[SmartSpeaker] ";
        app->execute();
    }
};

int main() {
    Smartwatch* watch = new Smartwatch();
    watch->installApp(new FitnessTracker());
    watch->runApp();

    SmartSpeaker* speaker = new SmartSpeaker();
    speaker->installApp(new MusicPlayer());
    speaker->runApp();

    delete watch;
    delete speaker;
    return 0;
}

Pontos-Chave do Bridge

  • O padrão Bridge utiliza "composição entre objetos" para desacoplar a abstração da implementação, permitindo que ambas variem em dimensões ortogonais (ou seja, possam ser "subclassificadas" independentemente).
  • Embora possa parecer uma alternativa à herança múltipla, a herança múltipla geralmente viola o Princípio da Responsabilidade Única (uma classe passa a ter mais de um motivo para mudar) e possui baixa reutilização. O Bridge é uma solução arquitetural muito superior.
  • A aplicação do Bridge é ideal quando existem dois eixos de variação muito fortes. Se houver mais de dois eixos, o padrão pode ser estendido para acomodar múltiplas pontes.
  • A transição de herança para composição é uma das técnicas mais poderosas para alcançar o desacoplamento e a flexibilidade no design orientado a objetos.
  • Em C++, uma classe que implementa apenas parcialmente as funções virtuais puras de sua base continua sendo uma classe abstrata, o que é perfeitamente válido e útil na criação de abstrações refinadas intermediárias.

Tags: C++ design-patterns decorator-pattern bridge-pattern single-responsibility-principle

Publicado em 6-11 20:11 por Thomas