C++
Implementación de patrones de diseño en C ++
Buscar..
Introducción
En esta página, puede encontrar ejemplos de cómo se implementan los patrones de diseño en C ++. Para obtener detalles sobre estos patrones, puede consultar la documentación de patrones de diseño .
Observaciones
Un patrón de diseño es una solución general reutilizable para un problema que ocurre comúnmente dentro de un contexto dado en el diseño de software.
Patrón observador
La intención de Observer Pattern es definir una dependencia de uno a muchos entre objetos para que cuando un objeto cambie de estado, todos sus dependientes sean notificados y actualizados automáticamente.
El sujeto y los observadores definen la relación uno a muchos. Los observadores dependen del tema, de modo que cuando el estado del sujeto cambia, los observadores reciben una notificación. Dependiendo de la notificación, los observadores también pueden actualizarse con nuevos valores.
Este es el ejemplo del libro "Patrones de diseño" de Gamma.
#include <iostream>
#include <vector>
class Subject;
class Observer
{
public:
virtual ~Observer() = default;
virtual void Update(Subject&) = 0;
};
class Subject
{
public:
virtual ~Subject() = default;
void Attach(Observer& o) { observers.push_back(&o); }
void Detach(Observer& o)
{
observers.erase(std::remove(observers.begin(), observers.end(), &o));
}
void Notify()
{
for (auto* o : observers) {
o->Update(*this);
}
}
private:
std::vector<Observer*> observers;
};
class ClockTimer : public Subject
{
public:
void SetTime(int hour, int minute, int second)
{
this->hour = hour;
this->minute = minute;
this->second = second;
Notify();
}
int GetHour() const { return hour; }
int GetMinute() const { return minute; }
int GetSecond() const { return second; }
private:
int hour;
int minute;
int second;
};
class DigitalClock: public Observer
{
public:
explicit DigitalClock(ClockTimer& s) : subject(s) { subject.Attach(*this); }
~DigitalClock() { subject.Detach(*this); }
void Update(Subject& theChangedSubject) override
{
if (&theChangedSubject == &subject) {
Draw();
}
}
void Draw()
{
int hour = subject.GetHour();
int minute = subject.GetMinute();
int second = subject.GetSecond();
std::cout << "Digital time is " << hour << ":"
<< minute << ":"
<< second << std::endl;
}
private:
ClockTimer& subject;
};
class AnalogClock: public Observer
{
public:
explicit AnalogClock(ClockTimer& s) : subject(s) { subject.Attach(*this); }
~AnalogClock() { subject.Detach(*this); }
void Update(Subject& theChangedSubject) override
{
if (&theChangedSubject == &subject) {
Draw();
}
}
void Draw()
{
int hour = subject.GetHour();
int minute = subject.GetMinute();
int second = subject.GetSecond();
std::cout << "Analog time is " << hour << ":"
<< minute << ":"
<< second << std::endl;
}
private:
ClockTimer& subject;
};
int main()
{
ClockTimer timer;
DigitalClock digitalClock(timer);
AnalogClock analogClock(timer);
timer.SetTime(14, 41, 36);
}
Salida:
Digital time is 14:41:36
Analog time is 14:41:36
Aquí está el resumen del patrón:
Los objetos (objeto
DigitalClock
oAnalogClock
) utilizan las interfaces de Asunto (Attach()
oDetach()
) para suscribirse (registrarse) como observadores o anular la suscripción (eliminar) de ser observadores (subject.Attach(*this);
subject.Detach(*this);
Cada sujeto puede tener muchos observadores (
vector<Observer*> observers;
).Todos los observadores necesitan implementar la interfaz de observador. Esta interfaz solo tiene un método,
Update()
, que se llama cuando cambia el estado del sujeto (Update(Subject &)
)Además de los métodos
Attach()
yDetach()
, el sujeto concreto implementa un métodoNotify()
que se utiliza para actualizar a todos los observadores actuales cada vez que cambia el estado. Pero en este caso, todos ellos se realizan en la clase principal,Subject
(Subject::Attach (Observer&)
,void Subject::Detach(Observer&)
yvoid Subject::Notify()
.El objeto concreto también puede tener métodos para establecer y obtener su estado.
Los observadores concretos pueden ser de cualquier clase que implemente la interfaz Observer. Cada observador se suscribe (registra) con un sujeto concreto para recibir la actualización (
subject.Attach(*this);
).Los dos objetos de Observer Pattern están ligeramente acoplados , pueden interactuar pero con poco conocimiento el uno del otro.
Variación:
Señal y ranuras
Signals and Slots es una construcción de lenguaje introducida en Qt, que facilita la implementación del patrón Observer y evita el código repetitivo. El concepto es que los controles (también conocidos como widgets) pueden enviar señales que contienen información de eventos que pueden ser recibidos por otros controles utilizando funciones especiales conocidas como ranuras. La ranura en Qt debe ser un miembro de la clase declarado como tal. El sistema de señal / ranura encaja bien con la forma en que se diseñan las interfaces gráficas de usuario. De manera similar, el sistema de señal / ranura se puede usar para notificaciones de eventos de E / S asíncronas (incluidos sockets, tuberías, dispositivos serie, etc.) o para asociar eventos de tiempo de espera con instancias de objetos, métodos o funciones apropiadas. No es necesario escribir ningún código de registro / cancelación de registro / invocación, porque el Meta Object Compiler (MOC) de Qt genera automáticamente la infraestructura necesaria.
El lenguaje C # también admite una construcción similar, aunque con una terminología y una sintaxis diferentes: los eventos desempeñan el papel de las señales y los delegados son los espacios. Además, un delegado puede ser una variable local, como un puntero a una función, mientras que una ranura en Qt debe ser un miembro de la clase declarado como tal.
Patrón de adaptador
Convertir la interfaz de una clase en otra interfaz que los clientes esperan. El adaptador (o Wrapper) permite que las clases trabajen juntas y que de otra manera no podrían debido a interfaces incompatibles. La motivación del patrón de adaptador es que podemos reutilizar el software existente si podemos modificar la interfaz.
Patrón de adaptador se basa en la composición del objeto.
El cliente llama a la operación en el objeto adaptador.
El adaptador llama a Adaptee para realizar la operación.
En STL, pila adaptada del vector: cuando la pila ejecuta push (), el vector subyacente hace vector :: push_back ().
Ejemplo:
#include <iostream>
// Desired interface (Target)
class Rectangle
{
public:
virtual void draw() = 0;
};
// Legacy component (Adaptee)
class LegacyRectangle
{
public:
LegacyRectangle(int x1, int y1, int x2, int y2) {
x1_ = x1;
y1_ = y1;
x2_ = x2;
y2_ = y2;
std::cout << "LegacyRectangle(x1,y1,x2,y2)\n";
}
void oldDraw() {
std::cout << "LegacyRectangle: oldDraw(). \n";
}
private:
int x1_;
int y1_;
int x2_;
int y2_;
};
// Adapter wrapper
class RectangleAdapter: public Rectangle, private LegacyRectangle
{
public:
RectangleAdapter(int x, int y, int w, int h):
LegacyRectangle(x, y, x + w, y + h) {
std::cout << "RectangleAdapter(x,y,x+w,x+h)\n";
}
void draw() {
std::cout << "RectangleAdapter: draw().\n";
oldDraw();
}
};
int main()
{
int x = 20, y = 50, w = 300, h = 200;
Rectangle *r = new RectangleAdapter(x,y,w,h);
r->draw();
}
//Output:
//LegacyRectangle(x1,y1,x2,y2)
//RectangleAdapter(x,y,x+w,x+h)
Resumen del código:
El cliente cree que está hablando con un
Rectangle
El objetivo es la clase
Rectangle
. Esto es en lo que el cliente invoca el método.Rectangle *r = new RectangleAdapter(x,y,w,h); r->draw();
Tenga en cuenta que la clase de adaptador utiliza herencia múltiple.
class RectangleAdapter: public Rectangle, private LegacyRectangle { ... }
El adaptador
RectangleAdapter
permite queLegacyRectangle
responda a request (draw()
en unRectangle
) heredando AMBAS clases.La clase
LegacyRectangle
no tiene los mismos métodos (draw()
) queRectangle
, pero elAdapter(RectangleAdapter)
puede tomar las llamadas al métodoRectangle
y girar e invocar el método en elLegacyRectangle
,oldDraw()
.class RectangleAdapter: public Rectangle, private LegacyRectangle { public: RectangleAdapter(int x, int y, int w, int h): LegacyRectangle(x, y, x + w, y + h) { std::cout << "RectangleAdapter(x,y,x+w,x+h)\n"; } void draw() { std::cout << "RectangleAdapter: draw().\n"; oldDraw(); } };
El patrón de diseño del adaptador traduce la interfaz para una clase en una interfaz compatible pero diferente. Por lo tanto, esto es similar al patrón de proxy en que es un contenedor de un solo componente. Pero la interfaz para la clase de adaptador y la clase original pueden ser diferentes.
Como hemos visto en el ejemplo anterior, este patrón de adaptador es útil para exponer una interfaz diferente para una API existente para permitir que funcione con otro código. Además, al usar un patrón de adaptador, podemos tomar interfaces heterogéneas y transformarlas para proporcionar una API consistente.
El patrón de Bridge tiene una estructura similar a un adaptador de objetos, pero Bridge tiene una intención diferente: está destinado a separar una interfaz de su implementación para que puedan variarse de forma fácil e independiente. Un adaptador está destinado a cambiar la interfaz de un objeto existente .
Patrón de fábrica
El patrón de fábrica desacopla la creación de objetos y permite la creación por nombre utilizando una interfaz común:
class Animal{
public:
virtual std::shared_ptr<Animal> clone() const = 0;
virtual std::string getname() const = 0;
};
class Bear: public Animal{
public:
virtual std::shared_ptr<Animal> clone() const override
{
return std::make_shared<Bear>(*this);
}
virtual std::string getname() const override
{
return "bear";
}
};
class Cat: public Animal{
public:
virtual std::shared_ptr<Animal> clone() const override
{
return std::make_shared<Cat>(*this);
}
virtual std::string getname() const override
{
return "cat";
}
};
class AnimalFactory{
public:
static std::shared_ptr<Animal> getAnimal( const std::string& name )
{
if ( name == "bear" )
return std::make_shared<Bear>();
if ( name == "cat" )
return std::shared_ptr<Cat>();
return nullptr;
}
};
Patrón de constructor con API fluida
El patrón del generador desacopla la creación del objeto del propio objeto. La idea principal detrás es que un objeto no tiene que ser responsable de su propia creación . El ensamblaje correcto y válido de un objeto complejo puede ser una tarea complicada en sí misma, por lo que esta tarea puede delegarse a otra clase.
Inspirado por el Email Builder en C # , he decidido hacer una versión de C ++ aquí. Un objeto de correo electrónico no es necesariamente un objeto muy complejo , pero puede demostrar el patrón.
#include <iostream>
#include <sstream>
#include <string>
using namespace std;
// Forward declaring the builder
class EmailBuilder;
class Email
{
public:
friend class EmailBuilder; // the builder can access Email's privates
static EmailBuilder make();
string to_string() const {
stringstream stream;
stream << "from: " << m_from
<< "\nto: " << m_to
<< "\nsubject: " << m_subject
<< "\nbody: " << m_body;
return stream.str();
}
private:
Email() = default; // restrict construction to builder
string m_from;
string m_to;
string m_subject;
string m_body;
};
class EmailBuilder
{
public:
EmailBuilder& from(const string &from) {
m_email.m_from = from;
return *this;
}
EmailBuilder& to(const string &to) {
m_email.m_to = to;
return *this;
}
EmailBuilder& subject(const string &subject) {
m_email.m_subject = subject;
return *this;
}
EmailBuilder& body(const string &body) {
m_email.m_body = body;
return *this;
}
operator Email&&() {
return std::move(m_email); // notice the move
}
private:
Email m_email;
};
EmailBuilder Email::make()
{
return EmailBuilder();
}
// Bonus example!
std::ostream& operator <<(std::ostream& stream, const Email& email)
{
stream << email.to_string();
return stream;
}
int main()
{
Email mail = Email::make().from("[email protected]")
.to("[email protected]")
.subject("C++ builders")
.body("I like this API, don't you?");
cout << mail << endl;
}
Para versiones anteriores de C ++, uno puede simplemente ignorar la operación std::move
y eliminar el && del operador de conversión (aunque esto creará una copia temporal).
El constructor finaliza su trabajo cuando libera el correo electrónico creado por el operator Email&&()
. En este ejemplo, el constructor es un objeto temporal y devuelve el correo electrónico antes de ser destruido. También puede usar una operación explícita como Email EmailBuilder::build() {...}
lugar del operador de conversión.
Pasar el constructor alrededor
Una gran característica que proporciona el patrón de creación es la capacidad de usar varios actores para construir un objeto juntos. Esto se hace pasando el constructor a los otros actores que cada uno dará más información al objeto construido. Esto es especialmente poderoso cuando está construyendo algún tipo de consulta, agregando filtros y otras especificaciones.
void add_addresses(EmailBuilder& builder)
{
builder.from("[email protected]")
.to("[email protected]");
}
void compose_mail(EmailBuilder& builder)
{
builder.subject("I know the subject")
.body("And the body. Someone else knows the addresses.");
}
int main()
{
EmailBuilder builder;
add_addresses(builder);
compose_mail(builder);
Email mail = builder;
cout << mail << endl;
}
Variante de diseño: objeto mutable
Puede cambiar el diseño de este patrón para adaptarse a sus necesidades. Te daré una variante.
En el ejemplo dado, el objeto de correo electrónico es inmutable, es decir, sus propiedades no se pueden modificar porque no hay acceso a ellas. Esta era una característica deseada. Si necesita modificar el objeto después de su creación, debe proporcionarle algunos configuradores. Dado que esos definidores se duplicarían en el constructor, puede considerar hacerlo todo en una clase (ya no se necesita ninguna clase de constructor). Sin embargo, consideraría la necesidad de hacer que el objeto construido sea mutable en primer lugar.