Recherche…


Introduction

Sur cette page, vous trouverez des exemples d'implémentation de modèles de conception en C ++. Pour plus de détails sur ces modèles, vous pouvez consulter la documentation sur les modèles de conception .

Remarques

Un modèle de conception est une solution réutilisable générale à un problème courant dans un contexte donné lors de la conception de logiciels.

Motif d'observateur

L'intention de Observer Pattern est de définir une dépendance un à plusieurs entre les objets, de sorte que lorsqu'un objet change d'état, tous ses dépendants sont notifiés et mis à jour automatiquement.

Le sujet et les observateurs définissent la relation un-à-plusieurs. Les observateurs dépendent du sujet de telle sorte que lorsque l'état du sujet change, les observateurs sont notifiés. Selon la notification, les observateurs peuvent également être mis à jour avec de nouvelles valeurs.

Voici l'exemple du livre "Design Patterns" 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);
}

Sortie:

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

Voici le résumé du motif:

  1. Les objets (objet DigitalClock ou AnalogClock ) utilisent les interfaces Subject ( Attach() ou Detach() ) pour s'inscrire (s'inscrire) en tant qu'observateur ou se désabonner (supprimer) de la fonction d'observateur ( subject.Attach(*this); subject.Detach(*this);

  2. Chaque sujet peut avoir plusieurs observateurs (observateurs vector<Observer*> observers; ).

  3. Tous les observateurs doivent implémenter l'interface Observer. Cette interface a juste une méthode, Update() , qui est appelée lorsque l'état du sujet change ( Update(Subject &) )

  4. Outre les méthodes Attach() et Detach() , le sujet concret implémente une méthode Notify() qui permet de mettre à jour tous les observateurs actuels chaque fois que l’état change. Mais dans ce cas, tous sont faits dans la classe parente, Subject ( Subject::Attach (Observer&) , void Subject::Detach(Observer&) et void Subject::Notify() .

  5. L'objet Concrete peut également avoir des méthodes pour définir et obtenir son état.

  6. Les observateurs concrets peuvent être n'importe quelle classe qui implémente l'interface Observer. Chaque observateur s'abonne (s'inscrire) à un sujet concret pour recevoir la mise à jour ( subject.Attach(*this); ).

  7. Les deux objets de Observer Pattern sont faiblement couplés , ils peuvent interagir mais avec peu de connaissance les uns des autres.

Variation:

Signal et Slots

Les signaux et les slots sont une construction de langage introduite dans Qt, ce qui facilite l'implémentation du pattern Observer tout en évitant le code passe-partout. Le concept est que les contrôles (également appelés widgets) peuvent envoyer des signaux contenant des informations d'événement pouvant être reçues par d'autres contrôles utilisant des fonctions spéciales appelées slots. L'emplacement dans Qt doit être un membre de classe déclaré comme tel. Le système de signal / logement correspond bien à la conception des interfaces utilisateur graphiques. De même, le système de signal / logement peut être utilisé pour la notification d'événement des E / S asynchrones (y compris les sockets, les tubes, les périphériques série, etc.) ou pour associer des événements de temporisation aux instances et méthodes ou fonctions d'objet appropriées. Aucun code d'inscription / de désenregistrement / d'invocation n'a besoin d'être écrit, car le compilateur de méta-objets (MOC) de Qt génère automatiquement l'infrastructure nécessaire.

Le langage C # prend également en charge un concept similaire, bien qu’avec une terminologie et une syntaxe différentes: les événements jouent le rôle de signaux et les délégués sont les emplacements. De plus, un délégué peut être une variable locale, un peu comme un pointeur de fonction, alors qu'un emplacement dans Qt doit être un membre de classe déclaré comme tel.

Modèle d'adaptateur

Convertir l'interface d'une classe en une autre interface attendue par les clients. Adapter (ou Wrapper) permet aux classes de fonctionner ensemble, ce qui ne pourrait pas être dû à des interfaces incompatibles. La motivation du modèle d'adaptateur est que nous pouvons réutiliser un logiciel existant si nous pouvons modifier l'interface.

  1. Le modèle d'adaptateur repose sur la composition de l'objet.

  2. Opération d'appels clients sur l'objet adaptateur.

  3. L'adaptateur appelle Adaptee pour effectuer l'opération.

  4. En STL, pile adaptée du vecteur: Lorsque la pile exécute push (), le vecteur sous-jacent fait vector :: push_back ().

Exemple:

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

Récapitulatif du code:

  1. Le client pense qu'il parle à un Rectangle

  2. La cible est la classe Rectangle . C'est ce que le client appelle la méthode.

     Rectangle *r = new RectangleAdapter(x,y,w,h);
     r->draw();
    
  3. Notez que la classe d'adaptateur utilise plusieurs héritages.

     class RectangleAdapter: public Rectangle, private LegacyRectangle {
         ...
     }
    
  4. Adapter RectangleAdapter permet à LegacyRectangle répondre aux requêtes ( draw() sur un Rectangle ) en héritant des deux classes.

  5. La classe LegacyRectangle n'a pas les mêmes méthodes ( draw() ) que Rectangle , mais l' Adapter(RectangleAdapter) peut prendre les appels de méthode Rectangle et LegacyRectangle la méthode sur 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();
         }
     };
    

Le modèle de conception de l' adaptateur traduit l'interface d'une classe en une interface compatible mais différente. Donc, ceci est similaire au modèle de proxy en ce qu’il s’agit d’un wrapper à un seul composant. Mais l'interface pour la classe d'adaptateur et la classe d'origine peuvent être différentes.

Comme nous l'avons vu dans l'exemple ci-dessus, ce modèle d' adaptateur est utile pour exposer une interface différente pour une API existante afin de lui permettre de fonctionner avec un autre code. De plus, en utilisant un modèle d'adaptateur, nous pouvons prendre des interfaces hétérogènes et les transformer pour fournir une API cohérente.

Le pont a une structure similaire à un adaptateur d'objet, mais Bridge a une intention différente: il est conçu pour séparer une interface de son implémentation afin de pouvoir la modifier facilement et indépendamment. Un adaptateur est destiné à modifier l'interface d'un objet existant .

Modèle d'usine

Le motif d'usine dissocie la création d'objet et permet la création par nom en utilisant une interface commune:

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


};

Modèle de générateur avec API Fluent

Le modèle de générateur dissocie la création de l'objet de l'objet lui-même. L'idée principale est qu'un objet ne doit pas nécessairement être responsable de sa propre création . L'assemblage correct et valide d'un objet complexe peut être une tâche compliquée en soi, cette tâche peut donc être déléguée à une autre classe.

Inspiré par Email Builder en C # , j'ai décidé de faire une version C ++ ici. Un objet Email n'est pas nécessairement un objet très complexe , mais il peut démontrer le motif.

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

Pour les anciennes versions de C ++, il suffit d'ignorer l'opération std::move et de supprimer le && de l'opérateur de conversion (bien que cela crée une copie temporaire).

Le générateur termine son travail lorsqu'il libère l'e- operator Email&&() par l' operator Email&&() . Dans cet exemple, le générateur est un objet temporaire et renvoie le courrier électronique avant sa destruction. Vous pouvez également utiliser une opération explicite comme Email EmailBuilder::build() {...} au lieu de l'opérateur de conversion.

Passer le constructeur autour

Une caractéristique intéressante du modèle de création est la possibilité d' utiliser plusieurs acteurs pour construire un objet ensemble. Cela se fait en passant le constructeur aux autres acteurs qui donneront chacun plus d'informations à l'objet construit. Ceci est particulièrement puissant lorsque vous créez une sorte de requête, en ajoutant des filtres et d'autres spécifications.

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 conception: objet Mutable

Vous pouvez modifier la conception de ce modèle pour répondre à vos besoins. Je vais donner une variante.

Dans l'exemple donné, l'objet Email est immuable, c'est-à-dire que ses propriétés ne peuvent pas être modifiées car il n'y a pas d'accès. C'était une fonctionnalité souhaitée. Si vous devez modifier l'objet après sa création, vous devez lui en fournir. Étant donné que ces paramètres seraient dupliqués dans le générateur, vous pouvez envisager de tout faire en une seule classe (aucune classe de générateur plus nécessaire). Néanmoins, je considérerais le besoin de rendre l'objet construit mutable en premier lieu.



Modified text is an extract of the original Stack Overflow Documentation
Sous licence CC BY-SA 3.0
Non affilié à Stack Overflow