# Les Principes SOLID --- ## Pourquoi parler DESIGN ? --- >"(…) when you program, you have to think about how someone will read your code, not just how a computer will interpret it." (Kent Beck) --- * Code écrit 1x, **lu** 10x * Evolutivité, maintenabilité, testabilité * Performance --- ## Bref historique --- * **Robert C. Martin** - "Design Principles and Design Patterns" (2000) * **Michael Feathers** - "Working effectively with Legacy Code" * **Barbara Liskov** - Doctorat en 1968, Prix Neumann en 2004, Turing Award en 2008 --- <div align="left"> **S**ingle Responsibility **O**pen/closed **L**iskov substitution **I**nterface segregation **D**ependency inversion </div> --- ## Single Responsibility Principle --- >Un élément ne doit avoir qu'un seul objectif qu'une et une seule raison de changer. * moins de modifications, moins de regressions * moins d'interdépendances, moins d'effets de bord * facilite tests, implémentation, maintenance * facilite la compréhension * que fait ma classe ? **et** <aside class="notes"> Pourquoi ? Facilite grandement l'implémentation et la maintenance en prévenant les effets de bords malheureux des futures évolutions. Les exigences évoluent au fil du temps de la vie d'une classe. Plus une classe a de responsabilités, plus souvent vous aurez besoin de la faire évoluer. Si votre classe a plusieurs responsabilités, elles deviennent dépendantes (modifier l'une implique de verifier que les autres sont toujours respectées) => complexifie les tests ! Le plus souvent vous modifier du code, le plus de chances vous avez d'introduire des regressions. Et ceci se répercute dans toutes les classes et composants qui en dépendent... (En C++, ça veut aussi dire plus de fichiers à recompiler) En résumé, plusieurs responsabilités => modifications plus fréquentes => modifications plus compliquées => plus d'effets de bord => plus de travail Un bénéfice important est la facilité d'explication, de compréhension et donc d'implémentation. Puis ensuite, une plus grande facilité de lecture et donc de compréhension du code. Ceci amène à moins de bugs et rend votre vie plus facile ! Semble facile, mais dans la vraie vie, les évolutions sont "justes un petit truc en plus" et on ne va pas reprendre le design pour ça. Il semble souvent plus simple et plus rapide d'ajouter juste une méthode ou faire deux/trois modifications par ci par là. Pour savoir si on respecte ce principe, il suffit de se demander ce que fait la classe/composant/méthode : si un "et" apparait dans la réponse, on va très certainement à l'encontre du principe. Il vaut mieux revenir en arrière et repenser votre approche. </aside> --- ```c++ class Person { private: // Civil status std::string firstName; std::string lastName; // Adress uint8_t streeNumber; std::string streetName; std::string city; uint8_t postalCode; }; ``` --- ```c++ class Address { private: uint8_t streeNumber; std::string streetName; std::string city; uint8_t postalCode; }; ``` ```c++ class Person{ private: // Etat civil std::string firstName; std::string lastName; Address homeAddress; }; ``` --- ## Open/Closed Principle --- > Les classes/modules/composants doivent être ouverts à l'extension mais fermés à la modification. * Comme la télévision * Ajouter du code sans modifier l'existant * Pas de modification des dépendances * Heritage vs Composition <aside class="notes"> Ce principe est certainement le plus important pour la conception objet. L'objectif est de pouvoir ajouter des nouvelles fonctionnalités sans changer le code existant. Pourquoi ? Cela permet d'éviter les situations où un changement requiert la modification des classes dépendantes. Parallèle avec une télévision : si je veux ajouter une nouvelle fonctionnalité (jouer avec une console), je ne dois pas ouvrir ma télé et souder la sortie de la console à l'intérieur de la télé : j'utilise un connecteur/une interface. Ceci peut-être réalisé avec l'héritage mais attention au couplage fort existant entre une classe mère et ses classes filles (C.F Liskov Substitution Principle). L'utilisation de l'agrégation et d'interfaces est conseillé : il est alors aisé d'ajouter des nouvelles fonctionnalités en développant de nouvelles classes et en substituant une classe à une autre. En effet l'interface ajoute un niveau d'abstraction qui rend le couplage plus lâche. Et ça n'êmpeche pas d'utiliser l'héritage s'il se justifie vraiment. Remarque : ceci implique qu'il est interdit d'utiliser la connaissance de la hierarchie de classe pour coder une fonction (on serait obligé de modifier ce code lors de l'ajout d'une sous-classe). </aside> --- ```c++ class Animal{ public: enum Kind{ NOT_DEFINED = 0, HORSE = 1, BIRD, FISH }; void moveToZone() const { switch (this->kindType){ case Animal::HORSE: this->moveToZoneForWalker(); break; case Animal::BIRD: this->moveToZoneForFlyer(); break; case Animal::FISH: this->moveToZoneForSwimmer(); break; default: //don't move break; } } private: Kind kindType; void moveToZoneForWalker() const; void moveToZoneForFlyer() const; void moveToZoneForSwimmer() const; }; ``` --- ```c++ class Animal{ public: virtual void moveToZone () const = 0; }; class Horse: public Animal{ public: void moveToZone() const{ std::cout << "I'm going to walker zone" << std::endl; } }; class Bird: public Animal{ public: void moveToZone() const{ std::cout << "I'm going to flying zone" << std::endl; } }; class Fish: public Animal{ public: void moveToZone() const{ std::cout << "I'm going to swimming zone" << std::endl; } }; class AnimalFactory{ public: enum AnimalKind{ NOT_DEFINED = 0, HORSE, BIRD, FISH }; const Animal* getAnimal(AnimalKind animalKind){ switch(animalKind){ case BIRD: return new Bird(); case FISH: return new Fish(); case HORSE: return new Horse(); default: throw new std::domain_error("Undefined kind of animal"); } } }; ``` --- ```c++ class Move { public: Move(); virtual void moveToZone() const = 0; protected: }; class MoveWalker : public Move { public: void moveToZone() const{ std::cout << "I'm going to walker zone" << std::endl; }; }; class MoveFlyer : public Move { public: void moveToZone() const{ std::cout << "I'm going to flying zone" << std::endl; }; }; class MoveSwimmer : public Move { public: void moveToZone() const{ std::cout << "I'm going to swimming zone" << std::endl; }; }; class Animal{ private: Move * movement; public: Animal(Move * movement) { this->movement = movement; }; void moveToZone () const { movement->moveToZone(); }; }; class AnimalFactory{ public: enum AnimalKind{ NOT_DEFINED = 0, HORSE, BIRD, FISH }; const Animal* getAnimal(AnimalKind animalKind){ switch(animalKind){ case BIRD: return new Animal(new MoveFlyer()); case FISH: return new Animal(new MoveSwimmer()); case HORSE: return new Animal(new MoveWalker()); default: throw new std::domain_error("Undefined kind of animal"); } }; }; ``` --- ## Liskov Substitution Principle --- >Si S est un sous-type de T, alors tout objet de type T peut être remplacé par un objet de type S sans altérer les propriétés désirables du programme concerné * Sous-classe <=> classe mère : **everywhere !** * au moins les mêmes input * au plus les mêmes output * mêmes exceptions (ou sous-types) * comportement >> structure <aside class="notes"> Implémenter ce principe revient à dire que le comportement d'une classe est plus important que sa structure. Il faut implémenter ses propres vérifications (Remarque : possibilité d'imposer ce principe avec la programmation par contrat via les préconditions et postconditions). Ce principe étend le précédent ("Ouvert/Fermer") en se focalisant l'héritage. Ce principe est tout aussi important, mais beaucoup plus dur à respecter. </aside> --- ```c++ class Bird { public: virtual void fly() { std::cout << "I'm flying"; }; virtual void lay() { std::cout << "I'm laying an egg"; }; }; class Duck : public Bird { }; class Ostrich : public Bird{ public: virtual void fly() { throw new std::domain_error("Ostrich cannot fly !"); }; }; ``` --- ```c++ class Bird { public: virtual void lay() { std::cout << "I'm laying an egg"; }; }; class Ostrich : public Bird { public: void run() { std::cout << "I'm running"; }; }; class FlyingBird : public Bird { public: virtual void fly() { std::cout << "I'm flying"; }; }; class Duck : public FlyingBird { }; ``` --- ## Interface Segregation Principle --- >Une classe ne devrait pas être forcée de dépendre d'interface qu'elle n'utilise pas. * **Attention** aux évolutions sans refactorer ! * Même combat que le SRP : * Limiter les dépendances * Limiter les impacts --- ```c++ class Message { public: std::string request() const; }; class IServer{ public: virtual void receive(Message& msg) const = 0; virtual void send(Message& msg) const = 0; virtual void saveInDatabase(const std::string& request) const = 0; virtual void getFromDatabase(const std::string& request) const = 0; }; class Server : public IServer { public: virtual void send(Message& msg) const { this->getFromDatabase(msg.request()); }; virtual void receive(Message& msg) const { this->saveInDatabase(msg.request()); }; virtual void getFromDatabase(const std::string& request) const {}; virtual void saveInDatabase(const std::string& request) const {}; }; ``` --- ```c++ class Message { public: std::string request() const; }; class IServer{ public: virtual void receive(Message& msg) const = 0; virtual void send(Message& msg) const = 0; }; class IPersistence { public: virtual void saveInDatabase(const std::string& request) = 0; virtual void getFromDatabase(const std::string& request) = 0; }; class MySQLPersistence: public IPersistence { virtual void saveInDatabase(const std::string& request) {}; virtual void getFromDatabase(const std::string& request) {}; }; class Server : public IServer { private: IPersistence * persister_; public: Server(IPersistence * persister) : persister_(persister) {}; virtual void receive(Message& msg) const { this->persister_->saveInDatabase(msg.request()); }; virtual void send(Message& msg) const { this->persister_->getFromDatabase(msg.request()); }; }; ``` --- ## Dependency Inversion Principle --- >Les modules de haut niveau ne doivent pas dépendre des modules de bas niveau. Les deux doivent dépendre d'abstractions. >Les abstractions ne doivent pas dépendre des détails. Les détails doivent dépendre des abstractions. --- * Réduire le couplage * Modules de haut niveau insensibles à la techniques * Augmenter la réutilisabilité * Testabilité --- ```c++ class TransactionManager { private: static TransactionManager * instance_; public: static const TransactionManager * INSTANCE() { if(instance_ == NULL) { instance_ = new TransactionManager(); } return instance_; } bool validatePaiement(long dateTransaction, double montantPaiement) {}; }; class TerminalClient{ public: Client(); bool transactions(double montant) { std::time_t today = std::time(0); const TransactionManager * banque = TransactionManager::INSTANCE(); return banque->validatePaiement(today, montant); } }; ``` --- ```c++ class IDate { public: virtual long secondsSinceEpoch() = 0; } class DateSystem : public IDate { private: long seconds_; public: DateSystem() : seconds_(std::time(0)){}; virtual long secondsSinceEpoch() { return this->seconds_; }; } class ITransactionManager { public: virtual bool validatePaiement(Date dateTransaction, double montantPaiement) = 0; }; class TransactionManager : public ITransactionManager { private: static TransactionManager * instance_; public: static const TransactionManager * INSTANCE() { if(instance_ == NULL) { instance_ = new TransactionManager(); } return instance_; } virtual bool validatePaiement(long secondsSinceEpoch, double montantPaiement) {}; }; class TerminalClient{ public: Client(); bool transactions(double montant) { IDate * now = new DateSystem();; const ITransactionManager * banque = TransactionManager::INSTANCE(); return banque->validatePaiement(now->secondsSinceEpoch(), montant); } }; ``` --- # Digression on Dependency Injection (not a SOLID principle, but...) --- > Les dépendances sont données à la construction de l'objet, l'objet ne crée pas lui même les objets dont il a besoin. * != Dependency Inversion Principle * Couplage faible * Souplesse * Testabilité et évolutivité --- ```c++ class IDate { public: virtual long secondsSinceEpoch() = 0; } class DateSystem : public IDate { private: long seconds_; public: DateSystem() : seconds_(std::time(0)){}; virtual long secondsSinceEpoch() { return this->seconds_; }; } class ITransactionManager { public: virtual bool validatePaiement(Date dateTransaction, double montantPaiement) = 0; }; class TransactionManager : public ITransactionManager { public: virtual bool validatePaiement(long secondsSinceEpoch, double montantPaiement) {}; }; class TerminalClient{ private: const ITransactionManager * banque_; public: Client(const ITransactionManager * banque) : banque_(banque) {}; bool transactions(IDate now, double montant) { return banque->validatePaiement(now.secondsSinceEpoch(), montant); } }; ``` --- # Conclusion --- Ces principes sont des objectifs à atteindre, des aident à la discussion, et méritent discussion à chaque fois. Ce sont des aides à la conception et au **refactoring** Autres notions : Clean code, YAGNI, KISS, DRY ...