2. C++ Avancé




2.12. Design Pattern

Un design pattern (ou patron de conception en français) est une solution conceptuelle à un problème logiciel donné.

On implante parfois sans le savoir des patrons de conception.

2.12.1. Façade

Exemple d'un design pattern qui n'en est pas un :

Façade : il permet de remédier à une situation complexe ou plusieurs classes ou modules interagissent entre-eux.

2.12.2. Singleton

Singleton : il permet de générer une seule instance de la classe.

Exemple : on peut l'utiliser dans le cas d'un jeux de paramètres qui sera partagé par plusieurs classes.

  1. #include <iostream>
  2. using namespace std;
  3.  
  4. /**
  5.  * class Parameters that implements Singleton Design Pattern
  6.  */
  7. class Parameters {
  8. protected:
  9.     int taxa_count;
  10.     int taxa_length;
  11.    
  12. private:
  13.     // *** must declare the instance static ***
  14.     static Parameters *instance;
  15.    
  16.     // default constructor
  17.     // all constructors must be *** private ***
  18.     Parameters() {
  19.         taxa_count = 0;
  20.         taxa_length = 0;
  21.     }      
  22.    
  23. public:
  24.     // *** must declare get_instance static ***
  25.     static Parameters& get_instance() {
  26.         if (instance == NULL) {
  27.             // call default constructor
  28.             instance = new Parameters;
  29.         }
  30.         return *instance;
  31.     }  
  32.    
  33.     // getters
  34.     int get_taxa_count() {
  35.         return taxa_count;
  36.     }
  37.    
  38.     int get_taxa_length() {
  39.         return taxa_length;
  40.     }
  41.    
  42.     // setters
  43.     void set_taxa_count(int n) {
  44.         taxa_count = n;
  45.     }
  46.     void set_taxa_length(int n) {
  47.         taxa_length = n;
  48.     }
  49.    
  50.     ostream& print(ostream& out) {
  51.         out << "taxa count  = " << taxa_count << endl;
  52.         out << "taxa length = " << taxa_length << endl;
  53.     }
  54.    
  55.     friend ostream& operator<<(ostream& out, Parameters& p) {
  56.         return p.print(out);
  57.     }
  58.    
  59. };
  60.  
  61. // don't forget to declare static instance of Parameters
  62. Parameters *Parameters::instance = NULL;
  63.  
  64. // main
  65. int main() {
  66.     Parameters& p = Parameters::get_instance();    
  67.    
  68.     p.set_taxa_count(10);
  69.     p.set_taxa_length(100);
  70.    
  71.     Parameters& q = Parameters::get_instance();
  72.     cout << q << endl;
  73.     return 0;
  74. }
  75.  

2.12.3. Factory

Factory : il permet de créer des instances qui ont quelque chose en commun.

Exemple: créer des personnes, étudiants, enseignants issus d'une hierarchie d'objets.

design pattern factory

  1. #include <iostream>
  2. #include <string>
  3. #include <vector>
  4. using namespace std;
  5.  
  6. /**
  7.  * base class Person with name and age
  8.  */
  9. class Person {
  10. protected:
  11.     string name;
  12.     int age;
  13. public:
  14.     Person(string n, int a) : name(n), age(a) {
  15.     }
  16.    
  17.     virtual ostream& print(ostream& out) {
  18.         out << "person " << name << ", " << age << endl;
  19.     }
  20.    
  21.     friend ostream& operator<<(ostream& out, Person& p) {
  22.         return p.print(out);
  23.     }
  24. };
  25.  
  26. /**
  27.  * Student is a Person that is registered to some Faculty
  28.  */
  29. class Student : public Person {
  30. protected:
  31.     string level;
  32. public:
  33.     Student(string n, int a, string l) : Person(n, a), level(l) {
  34.     }
  35.     ostream& print(ostream& out) {
  36.         out << "student " << name << ", " << age << " in " << level << endl;
  37.     }
  38.  
  39. };
  40.  
  41. /**
  42.  * Teacher is a Person that gets a salary
  43.  */
  44. class Teacher : public Person {
  45. protected:
  46.     float salary;
  47. public:
  48.     Teacher(string n, int a, float s) : Person(n, a), salary(s) {
  49.     }
  50.     ostream& print(ostream& out) {
  51.         out << "teacher " << name << ", " << age << " of salary $" << salary << endl;
  52.     }
  53.  
  54. };
  55.  
  56. /**
  57.  * Factory of Person, Student and Teacher
  58.  * we use polymorphism and have a 'create' method with different
  59.  * arguments
  60.  */
  61. class PersonFactory {
  62. public:
  63.     // create a Person
  64.     static Person *create(string n, int a) {
  65.         return new Person(n, a);
  66.     }
  67.    
  68.     // create a Student
  69.     static Person *create(string n, int a, string l) {
  70.         return dynamic_cast<Person *>(new Student(n, a, l));
  71.     }
  72.    
  73.     // create a Teacher
  74.     static Person *create(string n, int a, float s) {
  75.         return dynamic_cast<Person *>(new Teacher(n, a, s));
  76.     }
  77.    
  78. };
  79.  
  80. // main
  81. int main() {
  82.     vector<Person *> people;
  83.     Person *Tim = PersonFactory::create("Timothy", 25);
  84.     Person *Joe = PersonFactory::create("Joey", 20, "Computer Science");
  85.     Person *Sam = PersonFactory::create("Samantha", 21, "Medicine");
  86.     Person *Rod = PersonFactory::create("Rodney", 45, 3000.0);
  87.    
  88.     people.push_back(Tim);
  89.     people.push_back(Joe);
  90.     people.push_back(Sam);
  91.     people.push_back(Rod);
  92.    
  93.     for(Person *p : people) {
  94.         cout << *p ;
  95.     }
  96. }
  97.  
person Timothy, 25
student Joey, 20 in Computer Science
student Samantha, 21 in Medicine
teacher Rodney, 45 of salary $3000

2.12.4. Observer

Observer : il permet lors de la modification d'une donnée de mettre à jour d'autres classes.

Exemple: un dispose d'une série de valeurs qui est affichée deux fois sous forme de graphiques : un histogramme, un camembert. Si on modifie l'une des données (modification, ajout, suppression), on veut que les graphiques soient mis à jour.

  1. #include <iostream>
  2. #include <list>
  3. #include <string.h>
  4. #include <algorithm>
  5. using namespace std;
  6.  
  7. /**
  8.  * interface for Observer
  9.  */
  10. class Observer {
  11. public:
  12.     virtual void notify() = 0;
  13. };
  14.  
  15. /**
  16.  * base class for Subject
  17.  */
  18. class Subject {
  19. protected:
  20.     list<Observer *> l;
  21.  
  22.  public:
  23.     Subject() {
  24.     }
  25.    
  26.     virtual ~Subject() {
  27.     }
  28.    
  29.     void add_observer(Observer *o) {
  30.         l.push_back(o);
  31.     }
  32.    
  33.     void remove_observer(Observer *o) {
  34.         list<Observer *>::iterator it = find(l.begin(), l.end(), o);
  35.         if (it != l.end()) l.erase(it);
  36.     }
  37.    
  38.     void notify_observers() {
  39.         for (list<Observer *>::iterator it = l.begin(); it != l.end(); ++it) {
  40.             (*it)->notify();
  41.         }
  42.     }
  43. };
  44.  
  45. /**
  46.  * Concrete subject that contains an array of integers
  47.  *
  48.  */
  49. class DataSubject : public Subject {
  50. protected:
  51.     int *data;
  52.  
  53. public:
  54.     DataSubject() : Subject() {
  55.         data = new int [ 4 ];
  56.         memset(data, 0, 4 * sizeof(int));  
  57.     }
  58.    
  59.     ~DataSubject() {
  60.         delete [] data;
  61.     }
  62.    
  63.     void set(int n, int v) {
  64.         data[n] = v;
  65.         // notify observers on change
  66.         notify_observers();
  67.     }
  68.    
  69.     int get(int n) {
  70.         return data[n];
  71.     }
  72.  
  73. };
  74.  
  75. /**
  76.  * Concrete observer for Bar graph
  77.  */
  78. class BarGraph : public Observer {
  79. protected:
  80.     Subject *subject;
  81.  
  82. public:
  83.     BarGraph(Subject *s) : Observer() {
  84.         subject = s;
  85.     }
  86.    
  87.     ~BarGraph() {
  88.     }
  89.    
  90.     void notify() {
  91.         cout << "BarGraph : " << endl;
  92.         DataSubject *sub = dynamic_cast<DataSubject *>(subject);
  93.         for (int i=0; i<4; ++i) {
  94.             cout << sub->get(i) << " " ;
  95.         }
  96.         cout << endl;
  97.     }
  98. };
  99.  
  100. /**
  101.  * Concrete observer for Pie graph
  102.  */
  103. class PieGraph : public Observer {
  104. protected:
  105.     Subject *subject;
  106.  
  107. public:
  108.     PieGraph(Subject *s) : Observer() {
  109.         subject = s;
  110.     }
  111.    
  112.     ~PieGraph() {
  113.     }
  114.    
  115.     void notify() {
  116.         cout << "PieGraph : " << endl;
  117.         DataSubject *sub = dynamic_cast<DataSubject *>(subject);
  118.         for (int i=0; i<4; ++i) {
  119.             cout << sub->get(i) << " " ;
  120.         }
  121.         cout << endl;
  122.     }
  123. };
  124.  
  125.  
  126. // main
  127. int main() {
  128.     // create subject
  129.     DataSubject a_subject;
  130.  
  131.     // create observers
  132.     BarGraph bar( &a_subject );
  133.     PieGraph pie( &a_subject );
  134.  
  135.     // register observers
  136.     a_subject.add_observer( &bar );
  137.     a_subject.add_observer( &pie );
  138.  
  139.     // apply modification on data that will be notified to observers
  140.     a_subject.set(1,1);
  141.    
  142.     return 0;
  143. }
  144.  
  145.  
  146.  
  147.  

2.12.5. Autres design patterns

Voir sur Wikipedia

2.12.6. L'Inversion de contrôle et l'Injection de dépendance

L'inversion de contrôle (Inversion of Control, IoC) est un patron d'architecture commun aux frameworks. Voir sur Wikipedia pour un exemple explicatif.

L'injection de dépendances (Dependency Injection) est un mécanisme qui permet d'implémenter le principe de l'inversion de contrôle. Il consiste à créer dynamiquement (injecter) les dépendances entre les différentes classes en s'appuyant sur une description (fichier de configuration ou métadonnées) ou de manière programmatique. Ainsi les dépendances entre composants logiciels ne sont plus exprimées dans le code de manière statique mais déterminées dynamiquement à l'exécution (cf Wikipedia).

De manière plus simpliste, on peut expliquer l'injection de dépendance de la manière suivante : On considère un véhicule composé de 4 pneus et un moteur. Une fonction dépendant du kilométrage pourra mesurer l'usure du véhicule. Une implantation pour créer un véhicule est la suivante :

class Pneu { ... };

class Moteur { ... };

class Vehicule {
protected:
  Pneu *pneus[4];
  Moteur *moteur;
  float kilometrage;
  
public:
  /**
   * constructeur avec type de véhicule
   */
  Vehicule(int type) {
     if (type == _4x4) {
     	pneus[0] = new Goodyear();
     	pneus[1] = new Goodyear();
     	pneus[2] = new Goodyear();
     	pneus[3] = new Goodyear();
     	moteur = new LandRover();
     } else if (type == BERLINE) {
     	pneus[0] = new Michelin();
     	pneus[1] = new Michelin();
     	pneus[2] = new Michelin();
     	pneus[3] = new Michelin();
     	moteur = new Renault();
     }
     ...
  }  
  
  /**
   * calcul de l'usure en fonction du kilométrage
   */
  float usure() {
    ...
  }
};

Le défault de cette implantation est qu'elle est trop fortement couplée à Pneu et Moteur. Si on ajoute un nouveau type de véhicule, il faudra modifier le code de la classe.

Deux composants sont dits couplés s'ils échangent de l'information. Il existe différents types et niveaux de couplages (cf Wikipedia).

Une solution plus adaptée (ou conforme à l'injection de dépendance) consiste à passer en paramètre Pneus et Moteur, mais au final on ne fait que déplacer le problème.

class Vehicule {
protected:
  Pneu *pneus[4];
  Moteur *moteur;
  float kilometrage;
  
public:
  /**
   * constructeur avec pièces du véhicule
   */
  Vehicule(Moteur *m, Pneu *p[4]) {
   	pneus[0] = p[0];
   	pneus[1] = p[1];
   	pneus[2] = p[2];
   	pneus[3] = p[3];   	
   	moteur = m;
  }
    
  ...  
};