C++
Designmuster-Implementierung in C ++
Suche…
Einführung
Auf dieser Seite finden Sie Beispiele, wie Entwurfsmuster in C ++ implementiert werden. Einzelheiten zu diesen Mustern finden Sie in der Dokumentation zu den Entwurfsmustern .
Bemerkungen
Ein Entwurfsmuster ist eine allgemein wiederverwendbare Lösung für ein häufig auftretendes Problem in einem bestimmten Kontext des Softwareentwurfs.
Beobachtermuster
Observer Pattern hat die Absicht, eine Eins-zu-Viele-Abhängigkeit zwischen Objekten zu definieren, sodass alle abhängigen Objekte benachrichtigt werden und automatisch aktualisiert werden, wenn sich ein Objekt ändert.
Das Subjekt und die Beobachter definieren die Eins-zu-Viele-Beziehung. Die Beobachter sind vom Motiv abhängig, so dass die Beobachter benachrichtigt werden, wenn sich der Zustand des Probanden ändert. Je nach Benachrichtigung können die Beobachter auch mit neuen Werten aktualisiert werden.
Hier ist das Beispiel aus dem Buch "Design Patterns" von 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);
}
Ausgabe:
Digital time is 14:41:36
Analog time is 14:41:36
Hier ist die Zusammenfassung des Musters:
Objekte (
DigitalClock
oderAnalogClock
Objekt) verwenden , um den Betreff - Schnittstellen (Attach()
oderDetach()
) entweder abonnieren (Register) als Beobachter oder abmelden (entfernen) sich davon , dass Beobachter (subject.Attach(*this);
,subject.Detach(*this);
Jedes Subjekt kann viele Beobachter haben (
vector<Observer*> observers;
).Alle Beobachter müssen die Observer-Schnittstelle implementieren. Diese Schnittstelle hat nur eine Methode,
Update()
, die aufgerufen wird, wenn sich der Status des Betreffs ändert (Update(Subject &)
).Zusätzlich zu den Methoden
Attach()
undDetach()
implementiert das konkrete Subjekt eineNotify()
Methode, mit der alle aktuellen Beobachter aktualisiert werden, wenn sich der Status ändert. In diesem Fall werden jedoch alle in der übergeordneten KlasseSubject
(Subject::Attach (Observer&)
,void Subject::Detach(Observer&)
undvoid Subject::Notify()
.Das Concrete-Objekt kann auch über Methoden zum Setzen und Abrufen seines Status verfügen.
Konkrete Beobachter können jede Klasse sein, die die Observer-Schnittstelle implementiert. Jeder Beobachter abonniert (registrieren) sich mit einem konkreten Thema, um ein Update zu erhalten (
subject.Attach(*this);
).Die beiden Objekte des Observer Patterns sind lose miteinander verbunden , sie können miteinander interagieren, jedoch nur wenig voneinander wissen.
Variation:
Signal und Slots
Signale und Slots ist ein in Qt eingeführtes Sprachkonstrukt, das die Implementierung des Observer-Musters vereinfacht und den Boilerplate-Code vermeidet. Das Konzept besteht darin, dass Steuerelemente (auch als Widgets bezeichnet) Signale mit Ereignisinformationen senden können, die von anderen Steuerelementen mithilfe spezieller Funktionen (Slots) empfangen werden können. Der Slot in Qt muss ein als solches deklariertes Klassenmitglied sein. Das Signal / Slot-System passt gut zu der Art und Weise, wie grafische Benutzeroberflächen entworfen werden. In ähnlicher Weise kann das Signal / Slot-System für asynchrone E / A (einschließlich Sockets, Pipes, serielle Geräte usw.) Ereignisbenachrichtigung verwendet werden, oder um Zeitüberschreitungsereignisse mit geeigneten Objektinstanzen und Methoden oder Funktionen zu verknüpfen. Es muss kein Registrierungs- / Deregistrierungs- / Aufrufcode geschrieben werden, da der Meta Object Compiler (MOC) von Qt automatisch die benötigte Infrastruktur generiert.
Die C # -Sprache unterstützt auch ein ähnliches Konstrukt, jedoch mit einer anderen Terminologie und Syntax: Ereignisse spielen die Rolle von Signalen und Delegaten sind die Slots. Darüber hinaus kann ein Delegat eine lokale Variable sein, ähnlich wie ein Funktionszeiger, während ein Slot in Qt ein als solcher deklarierter Klassenmitglied sein muss.
Adaptermuster
Konvertieren Sie das Interface einer Klasse in ein anderes Interface, das die Clients erwarten. Mit Adapter (oder Wrapper) können Klassen zusammenarbeiten, die aufgrund von inkompatiblen Schnittstellen sonst nicht möglich wären. Die Motivation des Adapter-Patterns ist, dass wir vorhandene Software wiederverwenden können, wenn wir die Schnittstelle ändern können.
Das Adaptermuster hängt von der Objektzusammensetzung ab.
Client ruft den Vorgang für das Adapterobjekt auf.
Der Adapter ruft Adaptee auf, um die Operation auszuführen.
In STL, Stack vom Vektor angepasst: Wenn Stack push () ausführt, macht der darunterliegende Vektor vector :: push_back ().
Beispiel:
#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)
Zusammenfassung des Codes:
Der Kunde meint, er rede mit einem
Rectangle
Das Ziel ist die
Rectangle
Klasse. Daraufhin ruft der Client die Methode auf.Rectangle *r = new RectangleAdapter(x,y,w,h); r->draw();
Beachten Sie, dass die Adapterklasse mehrere Vererbung verwendet.
class RectangleAdapter: public Rectangle, private LegacyRectangle { ... }
Mit dem Adapter
RectangleAdapter
dasLegacyRectangle
auf request (draw()
auf einemRectangle
)LegacyRectangle
, indem es BEIDE Klassen erbt.Die
LegacyRectangle
Klasse verfügt nicht über die gleichen Methoden (draw()
) wieRectangle
, derAdapter(RectangleAdapter)
kann jedoch die Aufrufe derRectangle
MethodeLegacyRectangle
und dieoldDraw()
MethodeoldDraw()
.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(); } };
Das Design des Adapters übersetzt die Schnittstelle für eine Klasse in eine kompatible, aber andere Schnittstelle. Dies ähnelt dem Proxy- Muster, da es sich um einen Einkomponenten-Wrapper handelt. Die Schnittstelle für die Adapterklasse und die ursprüngliche Klasse kann jedoch unterschiedlich sein.
Wie im obigen Beispiel gezeigt wurde, ist dieses Adaptermuster hilfreich, um eine andere Schnittstelle für eine vorhandene API bereitzustellen, damit diese mit anderem Code arbeiten kann. Durch die Verwendung eines Adaptermusters können wir auch heterogene Schnittstellen verwenden und diese transformieren, um eine konsistente API bereitzustellen.
Bridge Pattern hat eine ähnliche Struktur wie ein Objektadapter, Bridge hat jedoch eine andere Absicht: Es soll eine Schnittstelle von ihrer Implementierung trennen , sodass sie leicht und unabhängig voneinander variiert werden kann. Ein Adapter soll die Schnittstelle eines vorhandenen Objekts ändern .
Fabrikmuster
Factory Pattern entkoppelt die Objekterstellung und ermöglicht die Erstellung anhand des Namens über eine gemeinsame Schnittstelle:
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;
}
};
Builder Pattern mit fließender API
Das Builder Pattern koppelt die Erstellung des Objekts vom Objekt selbst ab. Die Grundidee dahinter ist, dass ein Objekt nicht für seine eigene Erstellung verantwortlich sein muss . Die korrekte und gültige Assembly eines komplexen Objekts kann an sich eine komplizierte Aufgabe sein, daher kann diese Aufgabe an eine andere Klasse delegiert werden.
Inspiriert durch den Email Builder in C # habe ich mich entschlossen, hier eine C ++ - Version zu erstellen. Ein E-Mail-Objekt ist nicht unbedingt ein sehr komplexes Objekt , es kann jedoch das Muster demonstrieren.
#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;
}
Bei älteren Versionen von C ++ kann man den std::move
Vorgang einfach ignorieren und das && aus dem Konvertierungsoperator entfernen (obwohl dies eine temporäre Kopie erstellt).
Der Builder beendet seine Arbeit, wenn er die erstellte E-Mail vom operator Email&&()
freigibt. In diesem Beispiel ist der Builder ein temporäres Objekt und gibt die E-Mail zurück, bevor sie gelöscht wird. Sie können auch eine explizite Operation wie Email EmailBuilder::build() {...}
anstelle des Konvertierungsoperators verwenden.
Führe den Erbauer herum
Ein großartiges Feature des Builder Patterns ist die Möglichkeit, mehrere Darsteller zu verwenden, um ein Objekt zusammen zu erstellen. Dies geschieht, indem der Builder an die anderen Akteure übergeben wird, von denen jeder weitere Informationen zum erstellten Objekt liefert. Dies ist besonders nützlich, wenn Sie eine Art Abfrage erstellen und Filter und andere Spezifikationen hinzufügen.
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;
}
Designvariante: Objekt veränderlich
Sie können das Design dieses Musters an Ihre Bedürfnisse anpassen. Ich werde eine Variante geben.
Im angegebenen Beispiel ist das Email-Objekt unveränderlich, dh seine Eigenschaften können nicht geändert werden, da auf sie nicht zugegriffen werden kann. Dies war eine gewünschte Funktion. Wenn Sie das Objekt nach seiner Erstellung ändern müssen, müssen Sie einige Setzer angeben. Da diese Setter im Builder dupliziert werden, können Sie dies in einer Klasse erledigen (es ist keine Builder-Klasse mehr erforderlich). Trotzdem würde ich die Notwendigkeit in Betracht ziehen, das gebaute Objekt überhaupt veränderbar zu machen.