C++
Implementatie van ontwerppatronen in C ++
Zoeken…
Invoering
Op deze pagina vindt u voorbeelden van hoe ontwerppatronen in C ++ worden geïmplementeerd. Voor meer informatie over deze patronen kunt u de documentatie over ontwerppatronen raadplegen .
Opmerkingen
Een ontwerppatroon is een algemene herbruikbare oplossing voor een veelvoorkomend probleem binnen een gegeven context in softwareontwerp.
Waarnemer patroon
Het is de bedoeling van het waarnemerspatroon om een één-op-veel afhankelijkheid tussen objecten te definiëren, zodat alle afhankelijke personen automatisch op de hoogte worden gebracht wanneer een object van status verandert.
Het onderwerp en de waarnemers bepalen de één-op-veel-relatie. De waarnemers zijn afhankelijk van het onderwerp zodat wanneer de status van het onderwerp verandert, de waarnemers een melding ontvangen. Afhankelijk van de melding kunnen de waarnemers ook worden bijgewerkt met nieuwe waarden.
Hier is het voorbeeld uit het boek "Design Patterns" van 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);
}
Output:
Digital time is 14:41:36
Analog time is 14:41:36
Hier zijn de samenvatting van het patroon:
Objecten (
DigitalClock
ofAnalogClock
object) gebruiken de Subject-interfaces (Attach()
ofDetach()
) om zich als waarnemer te abonneren (registreren) of om zichzelf als waarnemer uit te schrijven (subject.Attach(*this);
subject.Detach(*this);
Elk onderwerp kan veel waarnemers hebben (
vector<Observer*> observers;
).Alle waarnemers moeten de Observer-interface implementeren. Deze interface heeft slechts één methode,
Update()
, die wordt aangeroepen wanneer de status van het onderwerp verandert (Update(Subject &)
)Naast de methoden
Attach()
enDetach()
implementeert het concrete onderwerp een methodeNotify()
die wordt gebruikt om alle huidige waarnemers bij te werken wanneer de status verandert. Maar in dit geval worden ze allemaal gedaan in de bovenliggende klasse,Subject
(Subject::Attach (Observer&)
,void Subject::Detach(Observer&)
envoid Subject::Notify()
.Het Concrete-object kan ook methoden hebben voor het instellen en verkrijgen van de status.
Concrete waarnemers kunnen elke klasse zijn die de Observer-interface implementeert. Elke waarnemer schrijft zich in (registreert) met een concreet onderwerp om update te ontvangen (
subject.Attach(*this);
).De twee objecten van Observer Pattern zijn losjes gekoppeld , ze kunnen communiceren, maar met weinig kennis van elkaar.
Variatie:
Signaal en slots
Signals and slots is een taalconstructie die in Qt is geïntroduceerd en waarmee het Observer-patroon eenvoudig kan worden geïmplementeerd zonder boilerplate-code te vermijden. Het concept is dat bedieningselementen (ook wel widgets genoemd) signalen met gebeurtenisinformatie kunnen verzenden die door andere bedieningselementen kunnen worden ontvangen met behulp van speciale functies die slots worden genoemd. De slot in Qt moet een als zodanig verklaard lid van de klas zijn. Het signaal / slotsysteem past goed bij de manier waarop grafische gebruikersinterfaces zijn ontworpen. Evenzo kan het signaal- / slotsysteem worden gebruikt voor asynchrone I / O (inclusief sockets, pipes, seriële apparaten, etc.) gebeurtenismeldingen of om time-outgebeurtenissen te koppelen aan geschikte objectinstanties en methoden of functies. Er hoeft geen registratie / uitschrijving / aanroepcode te worden geschreven, omdat Qt's Meta Object Compiler (MOC) automatisch de benodigde infrastructuur genereert.
De C # -taal ondersteunt ook een soortgelijk construct, hoewel met een andere terminologie en syntaxis: gebeurtenissen spelen de rol van signalen en afgevaardigden zijn de slots. Bovendien kan een gemachtigde een lokale variabele zijn, net als een functiepointer, terwijl een slot in Qt een klasse-lid moet zijn dat als zodanig wordt verklaard.
Adapterpatroon
Converteer de interface van een klasse naar een andere interface die clients verwachten. Adapter (of Wrapper) laat klassen samenwerken die anders niet konden vanwege incompatibele interfaces. De motivatie van het adapterpatroon is dat we bestaande software kunnen hergebruiken als we de interface kunnen wijzigen.
Adapterpatroon is afhankelijk van objectsamenstelling.
Client roept bewerking op adapterobject aan.
Adapter roept Adaptee op om de bewerking uit te voeren.
In STL, stapel aangepast van vector: wanneer stapel push () uitvoert, doet onderliggende vector vector :: push_back ().
Voorbeeld:
#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)
Samenvatting van de code:
De cliënt denkt dat hij tegen een
Rectangle
praatHet doel is de klasse
Rectangle
. Hierop roept de client de methode aan.Rectangle *r = new RectangleAdapter(x,y,w,h); r->draw();
Merk op dat de adapterklasse meerdere overerving gebruikt.
class RectangleAdapter: public Rectangle, private LegacyRectangle { ... }
Met de Adapter
RectangleAdapter
kan deLegacyRectangle
reageren op request (draw()
op eenRectangle
) door BEIDE klassen te erven.De
LegacyRectangle
klasse heeft niet dezelfde methoden (draw()
) alsRectangle
, maar deAdapter(RectangleAdapter)
kan de aanroepen van deRectangle
methode aannemen en zich omdraaien en de methode aanroepen op deLegacyRectangle
,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(); } };
Het ontwerppatroon van de adapter vertaalt de interface voor één klasse in een compatibele maar verschillende interface. Dit is dus vergelijkbaar met het proxy- patroon omdat het een wrapper met één component is. Maar de interface voor de adapterklasse en de originele klasse kan verschillen.
Zoals we in het bovenstaande voorbeeld hebben gezien, is dit adapterpatroon handig om een andere interface voor een bestaande API bloot te leggen zodat deze met andere code kan werken. Door het adapterpatroon te gebruiken, kunnen we ook heterogene interfaces nemen en deze transformeren tot een consistente API.
Bridge-patroon heeft een structuur die lijkt op een objectadapter, maar Bridge heeft een andere bedoeling: het is bedoeld om een interface te scheiden van de implementatie, zodat ze eenvoudig en onafhankelijk kunnen worden gevarieerd. Een adapter is bedoeld om de interface van een bestaand object te wijzigen.
Fabriek patroon
Fabriekspatroon ontkoppelt objectcreatie en maakt creatie op naam mogelijk via een gemeenschappelijke interface:
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;
}
};
Bouwerpatroon met vloeiende API
Het Builder-patroon ontkoppelt de creatie van het object van het object zelf. Het achterliggende idee is dat een object niet verantwoordelijk hoeft te zijn voor zijn eigen creatie . De juiste en geldige samenstelling van een complex object kan op zichzelf een ingewikkelde taak zijn, dus deze taak kan aan een andere klasse worden gedelegeerd.
Geïnspireerd door de Email Builder in C # , heb ik besloten om hier een C ++ -versie te maken. Een e-mailobject is niet noodzakelijkerwijs een zeer complex object , maar het kan het patroon demonstreren.
#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;
}
Voor oudere versies van C ++ kan men gewoon de bewerking std::move
negeren en de && verwijderen uit de conversie-operator (hoewel dit een tijdelijke kopie oplevert).
De bouwer voltooit zijn werk wanneer hij de ingebouwde e-mail van de operator Email&&()
. In dit voorbeeld is de builder een tijdelijk object en stuurt de e-mail terug voordat deze wordt vernietigd. U kunt ook een expliciete bewerking gebruiken zoals Email EmailBuilder::build() {...}
plaats van de conversieoperator.
Geef de bouwer door
Een geweldige functie die het Builder-patroon biedt, is de mogelijkheid om meerdere acteurs te gebruiken om samen een object te bouwen. Dit wordt gedaan door de bouwer door te geven aan de andere acteurs die elk wat meer informatie aan het gebouwde object zullen geven. Dit is met name krachtig wanneer u een soort query bouwt, filters en andere specificaties toevoegt.
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;
}
Ontwerpvariant: veranderlijk object
U kunt het ontwerp van dit patroon aanpassen aan uw behoeften. Ik geef een variant.
In het gegeven voorbeeld is het e-mailobject onveranderlijk, dat wil zeggen dat de eigenschappen ervan niet kunnen worden gewijzigd omdat er geen toegang tot is. Dit was een gewenste functie. Als u het object moet wijzigen nadat het is gemaakt, moet u er een aantal instellingen voor opgeven. Aangezien deze setters in de builder worden gedupliceerd, kunt u overwegen het allemaal in één klasse te doen (geen builderklasse meer nodig). Toch zou ik de noodzaak overwegen om het gebouwde object in de eerste plaats veranderlijk te maken.