Ricerca…


introduzione

In questa pagina, puoi trovare esempi di come i modelli di design sono implementati in C ++. Per i dettagli su questi modelli, è possibile consultare la documentazione dei modelli di progettazione .

Osservazioni

Un modello di progettazione è una soluzione generale riutilizzabile per un problema comune in un dato contesto nella progettazione del software.

Modello di osservatore

L'intento di Observer Pattern è di definire una dipendenza uno-a-molti tra gli oggetti in modo che quando un oggetto cambia stato, tutti i suoi dipendenti vengono notificati e aggiornati automaticamente.

Il soggetto e gli osservatori definiscono la relazione uno-a-molti. Gli osservatori dipendono dal soggetto in modo tale che quando lo stato del soggetto cambia, gli osservatori vengono avvisati. A seconda della notifica, gli osservatori possono anche essere aggiornati con nuovi valori.

Ecco l'esempio del libro "Design Patterns" di 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);
}

Produzione:

Digital time is 14:41:36
Analog time is 14:41:36

Ecco il riepilogo del modello:

  1. Gli oggetti (oggetto DigitalClock o AnalogClock ) utilizzano le interfacce oggetto ( Attach() o Detach() ) per iscriversi (registrarsi) come osservatori o annullare l'iscrizione (rimuovere) stessi dall'essere osservatori ( subject.Attach(*this); subject.Detach(*this);

  2. Ogni soggetto può avere molti osservatori (osservatori vector<Observer*> observers; ).

  3. Tutti gli osservatori devono implementare l'interfaccia Observer. Questa interfaccia ha solo un metodo, Update() , che viene chiamato quando lo stato del soggetto cambia ( Update(Subject &) )

  4. Oltre ai metodi Attach() e Detach() , il soggetto concreto implementa un metodo Notify() che viene utilizzato per aggiornare tutti gli osservatori correnti ogni volta che si verificano cambiamenti di stato. Ma in questo caso, tutti sono fatti nella classe genitore, Subject ( Subject::Attach (Observer&) , void Subject::Detach(Observer&) e void Subject::Notify() .

  5. L'oggetto Concrete può anche avere metodi per impostare e ottenere il suo stato.

  6. Gli osservatori concreti possono essere qualsiasi classe che implementa l'interfaccia Observer. Ogni osservatore si iscrive (registra) con un soggetto concreto per ricevere l'aggiornamento ( subject.Attach(*this); ).

  7. I due oggetti di Observer Pattern sono accoppiati liberamente , possono interagire ma con poca conoscenza l'uno dell'altro.

Variazione:

Segnale e slot

I segnali e gli slot sono un costrutto linguistico introdotto in Qt, che semplifica l'implementazione del pattern Observer evitando il codice boilerplate. Il concetto è che i controlli (noti anche come widget) possono inviare segnali contenenti informazioni sugli eventi che possono essere ricevuti da altri controlli utilizzando funzioni speciali conosciute come slot. Lo slot di Qt deve essere un membro della classe dichiarato come tale. Il sistema segnale / slot si adatta perfettamente al modo in cui sono progettate le interfacce utente grafiche. Analogamente, il sistema segnale / slot può essere utilizzato per la notifica di eventi I / O asincroni (inclusi socket, pipe, dispositivi seriali, ecc.) O per associare eventi di timeout a istanze e metodi o funzioni appropriati. Non è necessario scrivere codice di registrazione / cancellazione / chiamata, poiché il Meta Object Compiler (MOC) di Qt genera automaticamente l'infrastruttura necessaria.

Il linguaggio C # supporta anche un costrutto simile sebbene con una terminologia e sintassi diverse: gli eventi svolgono il ruolo di segnali e i delegati sono gli slot. Inoltre, un delegato può essere una variabile locale, proprio come un puntatore a funzione, mentre uno slot in Qt deve essere un membro di classe dichiarato come tale.

Modello adattatore

Converti l'interfaccia di una classe in un'altra interfaccia che i clienti si aspettano. Adapter (o Wrapper) consente alle classi di funzionare insieme che non potrebbero altrimenti a causa di interfacce incompatibili. La motivazione del modello Adapter è che possiamo riutilizzare il software esistente se possiamo modificare l'interfaccia.

  1. Il modello dell'adattatore si basa sulla composizione dell'oggetto.

  2. Operazione di chiamate client su oggetto Adapter.

  3. Adattatore chiama Adaptee per eseguire l'operazione.

  4. In AWL, stack adattato dal vettore: quando stack esegue push (), il vettore sottostante vector vector: push_back ().

Esempio:

#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)

Riassunto del codice:

  1. Il cliente pensa di parlare con un Rectangle

  2. L'obiettivo è la classe Rectangle . Questo è ciò su cui il client invoca il metodo.

     Rectangle *r = new RectangleAdapter(x,y,w,h);
     r->draw();
    
  3. Si noti che la classe dell'adattatore utilizza l'ereditarietà multipla.

     class RectangleAdapter: public Rectangle, private LegacyRectangle {
         ...
     }
    
  4. Adapter RectangleAdapter consente a LegacyRectangle rispondere alla richiesta ( draw() su un Rectangle ) ereditando le classi BOTH.

  5. La classe LegacyRectangle non ha gli stessi metodi ( draw() ) come Rectangle , ma Adapter(RectangleAdapter) può prendere le chiamate del metodo Rectangle e girare e richiamare il metodo su LegacyRectangle , 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();
         }
     };
    

Il modello di progettazione dell'adattatore traduce l'interfaccia per una classe in un'interfaccia compatibile ma diversa. Quindi, questo è simile al pattern proxy in quanto è un wrapper a componente singolo. Ma l'interfaccia per la classe dell'adattatore e la classe originale potrebbe essere diversa.

Come abbiamo visto nell'esempio sopra, questo modello di adattatore è utile per esporre un'interfaccia diversa per un'API esistente per consentirne il funzionamento con altro codice. Inoltre, utilizzando il modello di adattatore, possiamo prendere interfacce eterogenee e trasformarle per fornire API coerenti.

Il bridge pattern ha una struttura simile ad un object adapter, ma Bridge ha un intento diverso: è pensato per separare un'interfaccia dalla sua implementazione in modo che possano essere variate facilmente e indipendentemente. Un adattatore ha lo scopo di cambiare l'interfaccia di un oggetto esistente .

Modello di fabbrica

Il modello di fabbrica disaccoppia la creazione dell'oggetto e consente la creazione per nome utilizzando un'interfaccia comune:

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 with Fluent API

The Builder Pattern disaccoppia la creazione dell'oggetto dall'oggetto stesso. L'idea principale dietro è che un oggetto non deve essere responsabile della propria creazione . L'assemblaggio corretto e valido di un oggetto complesso può essere un'attività complessa di per sé, quindi questa attività può essere delegata a un'altra classe.

Ispirato da Email Builder in C # , ho deciso di creare una versione C ++ qui. Un oggetto di posta elettronica non è necessariamente un oggetto molto complesso , ma può dimostrare il modello.

#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;
}

Per le versioni precedenti di C ++, si può semplicemente ignorare l'operazione std::move e rimuovere il && dall'operatore di conversione (anche se ciò creerà una copia temporanea).

Il costruttore termina il proprio lavoro quando rilascia l'email creata operator Email&&() . In questo esempio, il builder è un oggetto temporaneo e restituisce l'e-mail prima di essere distrutto. Puoi anche utilizzare un'operazione esplicita come Email EmailBuilder::build() {...} posto dell'operatore di conversione.

Passa il costruttore in giro

Un'ottima caratteristica di Builder Pattern è la possibilità di utilizzare diversi attori per costruire un oggetto insieme. Questo viene fatto passando il builder agli altri attori che daranno un po 'più di informazioni all'oggetto costruito. Questo è particolarmente potente quando si costruisce una sorta di query, aggiungendo filtri e altre specifiche.

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 di design: oggetto mutabile

È possibile modificare il design di questo modello in base alle proprie esigenze. Darò una variante.

Nell'esempio fornito l'oggetto Email è immutabile, cioè le sue proprietà non possono essere modificate perché non c'è accesso ad esse. Questa era una caratteristica desiderata. Se è necessario modificare l'oggetto dopo la sua creazione, è necessario fornire alcuni setter ad esso. Dal momento che questi setter verrebbero duplicati nel builder, potresti considerare di fare tutto in una classe (non è più necessaria alcuna classe di builder). Tuttavia, considererei la necessità di rendere l'oggetto costruito mutevole in primo luogo.



Modified text is an extract of the original Stack Overflow Documentation
Autorizzato sotto CC BY-SA 3.0
Non affiliato con Stack Overflow