C++
Implementacja wzorca projektowego w C ++
Szukaj…
Wprowadzenie
Na tej stronie można znaleźć przykłady implementacji wzorców projektowych w C ++. Szczegółowe informacje na temat tych wzorów można znaleźć w dokumentacji wzorów projektowych .
Uwagi
Wzorzec projektowy jest ogólnym rozwiązaniem wielokrotnego użytku dla często występującego problemu w danym kontekście w projektowaniu oprogramowania.
Wzór obserwatora
Intencją Wzoru Obserwatora jest zdefiniowanie zależności jeden-do-wielu między obiektami, aby w przypadku zmiany stanu jednego obiektu wszystkie jego osoby zależne były powiadamiane i aktualizowane automatycznie.
Podmiot i obserwatorzy określają relację jeden do wielu. Obserwatorzy są zależni od podmiotu, tak że gdy zmienia się stan podmiotu, obserwatorzy są powiadamiani. W zależności od powiadomienia obserwatorzy mogą również zostać zaktualizowani o nowe wartości.
Oto przykład z książki „Wzory projektowe” Gammy.
#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);
}
Wynik:
Digital time is 14:41:36
Analog time is 14:41:36
Oto podsumowanie wzoru:
Obiekty (obiekt
DigitalClock
lubAnalogClock
) używają interfejsów podmiotu (Attach()
lubDetach()
) do subskrybowania (rejestracji) jako obserwatorzy lub do rezygnacji z subskrypcji (usuwania) z bycia obserwatorami (subject.Attach(*this);
subject.Detach(*this);
Każdy podmiot może mieć wielu obserwatorów (
vector<Observer*> observers;
).Wszyscy obserwatorzy muszą wdrożyć interfejs Observer. Ten interfejs ma tylko jedną metodę,
Update()
, która jest wywoływana, gdy zmienia się stan podmiotu (Update(Subject &)
)Oprócz metod
Attach()
iDetach()
konkretny podmiot implementuje metodęNotify()
, która jest używana do aktualizacji wszystkich bieżących obserwatorów przy każdej zmianie stanu. Ale w tym przypadku wszystkie z nich są wykonywane w klasie nadrzędnej,Subject
(Subject::Attach (Observer&)
,void Subject::Detach(Observer&)
ivoid Subject::Notify()
.Obiekt Beton może również mieć metody ustawiania i uzyskiwania swojego stanu.
Konkretnymi obserwatorami mogą być dowolne klasy, które implementują interfejs Observer. Każdy obserwator subskrybuje (rejestruje) konkretny temat, aby otrzymać aktualizację (temat.
subject.Attach(*this);
).Dwa obiekty Wzorca Obserwatora są luźno połączone , mogą oddziaływać, ale niewiele o sobie wiedzą.
Zmiana:
Sygnał i gniazda
Sygnały i szczeliny to konstrukcja językowa wprowadzona w Qt, która ułatwia implementację wzorca Observer przy jednoczesnym unikaniu kodu bojlera. Koncepcja polega na tym, że elementy sterujące (znane również jako widżety) mogą wysyłać sygnały zawierające informacje o zdarzeniach, które mogą być odbierane przez inne elementy sterujące za pomocą specjalnych funkcji zwanych gniazdami. Miejsce w Qt musi być członkiem klasy zadeklarowanym jako taki. System sygnału / gniazda jest dobrze dopasowany do sposobu graficznego interfejsu użytkownika. Podobnie system sygnał / szczelina może być wykorzystywany do asynchronicznego we / wy (w tym gniazd, rur, urządzeń szeregowych itp.) Do powiadamiania o zdarzeniach lub do kojarzenia zdarzeń przekroczenia limitu czasu z odpowiednimi instancjami i metodami lub funkcjami obiektu. Nie trzeba pisać kodu rejestracji / wyrejestrowania / wywołania, ponieważ kompilator Qa Meta Object Compiler (MOC) automatycznie generuje potrzebną infrastrukturę.
Język C # obsługuje również podobną konstrukcję, chociaż z inną terminologią i składnią: zdarzenia odgrywają rolę sygnałów, a delegaci są gniazdami. Ponadto delegat może być zmienną lokalną, podobnie jak wskaźnik funkcji, podczas gdy gniazdo w Qt musi być członkiem klasy zadeklarowanym jako taki.
Wzór adaptera
Konwertuj interfejs klasy na inny interfejs, którego oczekują klienci. Adapter (lub Wrapper) pozwala klasom współpracować, które nie mogłyby inaczej z powodu niekompatybilnych interfejsów. Motywacja wzorca adaptera polega na tym, że możemy ponownie wykorzystać istniejące oprogramowanie, jeśli możemy zmodyfikować interfejs.
Wzór adaptera zależy od kompozycji obiektu.
Klient wywołuje operację na obiekcie Adapter.
Adapter wzywa Adaptee do przeprowadzenia operacji.
W STL stos dostosowany z wektora: gdy stos wykonuje push (), wektor bazowy robi vector :: push_back ().
Przykład:
#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)
Podsumowanie kodu:
Klient myśli, że rozmawia z
Rectangle
Celem jest klasa
Rectangle
. To właśnie wywołuje metoda klienta.Rectangle *r = new RectangleAdapter(x,y,w,h); r->draw();
Należy zauważyć, że klasa adaptera korzysta z wielokrotnego dziedziczenia.
class RectangleAdapter: public Rectangle, private LegacyRectangle { ... }
Adapter
RectangleAdapter
pozwalaLegacyRectangle
na żądanie (draw()
naRectangle
) poprzez dziedziczenie OBU klas.Klasa
LegacyRectangle
nie ma takich samych metod (draw()
) jakRectangle
, aleAdapter(RectangleAdapter)
może odbierać wywołania metodRectangle
oraz zawracać i wywoływać metodę naLegacyRectangle
,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(); } };
Wzorzec projektu adaptera tłumaczy interfejs jednej klasy na kompatybilny, ale inny interfejs. Jest to więc podobne do wzorca proxy , ponieważ jest to opakowanie jednoskładnikowe. Ale interfejs dla klasy adaptera i klasy oryginalnej może być inny.
Jak widzieliśmy w powyższym przykładzie, ten wzorzec adaptera jest użyteczny do ujawnienia innego interfejsu dla istniejącego interfejsu API, aby umożliwić mu pracę z innym kodem. Ponadto za pomocą wzorca adaptera możemy pobierać heterogeniczne interfejsy i przekształcać je w celu zapewnienia spójnego interfejsu API.
Wzorzec mostka ma strukturę podobną do adaptera obiektowego, ale Bridge ma inne przeznaczenie: ma na celu oddzielenie interfejsu od jego implementacji, dzięki czemu można je łatwo i niezależnie zmieniać. Adapter ma na celu zmianę interfejsu istniejącego obiektu.
Wzór fabryczny
Wzorzec fabryczny oddziela tworzenie obiektów i umożliwia tworzenie według nazwy przy użyciu wspólnego interfejsu:
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;
}
};
Wzorzec konstruktora z płynnym API
Wzorzec konstruktora oddziela tworzenie obiektu od samego obiektu. Główną ideą jest to, że obiekt nie musi być odpowiedzialny za swoje własne stworzenie . Prawidłowe i prawidłowe złożenie złożonego obiektu może samo w sobie być skomplikowanym zadaniem, więc zadanie to można przekazać innej klasie.
Zainspirowany przez Konstruktora poczty e-mail w C # postanowiłem stworzyć tutaj wersję C ++. Obiekt e-mail niekoniecznie jest obiektem bardzo złożonym , ale może zademonstrować wzorzec.
#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;
}
W starszych wersjach C ++ można po prostu zignorować operację std::move
i usunąć && z operatora konwersji (chociaż spowoduje to utworzenie tymczasowej kopii).
Konstruktor kończy pracę, gdy wypuści wbudowaną wiadomość e-mail przez operator Email&&()
. W tym przykładzie konstruktor jest obiektem tymczasowym i zwraca wiadomość e-mail przed zniszczeniem. Możesz także użyć operacji jawnej, takiej jak Email EmailBuilder::build() {...}
zamiast operatora konwersji.
Podaj budowniczego
Świetną funkcją, jaką zapewnia Wzorzec konstruktora, jest możliwość użycia kilku aktorów do wspólnego zbudowania obiektu. Odbywa się to poprzez przekazanie konstruktora innym aktorom, którzy podadzą budowanym obiektom więcej informacji. Jest to szczególnie przydatne, gdy budujesz jakieś zapytanie, dodajesz filtry i inne specyfikacje.
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;
}
Wariant projektu: obiekt zmienny
Możesz zmienić projekt tego wzoru, aby dopasować go do swoich potrzeb. Dam jeden wariant.
W podanym przykładzie obiekt Email jest niezmienny, tzn. Jego właściwości nie można modyfikować, ponieważ nie ma do nich dostępu. To była pożądana funkcja. Jeśli musisz zmodyfikować obiekt po jego utworzeniu, musisz podać mu kilka ustawień. Ponieważ te elementy ustawiające zostałyby zduplikowane w kreatorze, możesz rozważyć zrobienie tego wszystkiego w jednej klasie (żadna klasa konstruktora nie jest już potrzebna). Niemniej jednak rozważyłbym przede wszystkim potrzebę zmodowania zbudowanego obiektu.