Introduction: SOLID in the Context of C++
SOLID est un acronyme pour cinq principes fondamentaux de conception orientée objet formulés par Robert Martin. Pour C++, en tant que langage avec un typage statique puissant, l'héritage multiple et RAII, ces principes prennent une signification particulière. À un niveau avancé, comprendre SOLID vous permet de créer des systèmes flexibles, testables et extensibles, en évitant les antipatterns tels que la « classe god » ou le couplage fort.
Dans cet article, nous décomposerons chaque principe avec des exemples de code C++ (17/20) pratiques, en nous concentrant sur les nuances d'implémentation : l'utilisation de std::unique_ptr, std::function, CRTP, les destructeurs virtuels et les idiomes modernes.
1. Principe de Responsabilité Unique (SRP)
Définition : Une classe ne devrait avoir qu'une seule raison de changer.
En pratique, cela signifie qu'une classe est responsable d'une seule entité métier ou d'une seule opération. Violer le SRP conduit à un « couplage » des responsabilités, ce qui complique les tests et la modification.
Exemple de Violation
class Report {public: void generate() { /* data generation */ } void saveToFile(const std::string& path) { /* file writing */ } void sendEmail(const std::string& to) { /* sending */ }};Ici, Report est responsable de la génération, de la sauvegarde et de l'envoi — trois raisons de changer.
Refactorisation avec SRP
class Report {public: void generate() { /* data generation */ }};
class ReportSaver {public: void save(const Report& report, const std::string& path);};
class EmailSender {public: void send(const Report& report, const std::string& to);};Maintenant, chaque classe a une responsabilité unique. Cela simplifie les tests unitaires et la réutilisation.
2. Principe Ouvert/Fermé (OCP)
Définition : Les entités logicielles doivent être ouvertes à l'extension mais fermées à la modification.
En C++, cela est souvent implémenté via le polymorphisme (fonctions virtuelles) ou les templates (CRTP). Pour un niveau avancé, nous considérerons les deux techniques.
Approche Polymorphe (Fonctions Virtuelles)
class Shape {public: virtual double area() const = 0; virtual ~Shape() = default;};
class Circle : public Shape { double r;public: Circle(double r) : r(r) {} double area() const override { return 3.14 * r * r; }};
class Rectangle : public Shape { double w, h;public: Rectangle(double w, double h) : w(w), h(h) {} double area() const override { return w * h; }};Ajouter une nouvelle forme (par exemple, Triangle) ne nécessite pas de modifier les classes existantes — seulement créer une nouvelle classe dérivée.
Approche par Template (CRTP — Curiously Recurring Template Pattern)
template<typename Derived>class ShapeBase {public: double area() const { return static_cast<const Derived*>(this)->area_impl(); }};
class Circle : public ShapeBase<Circle> { double r;public: Circle(double r) : r(r) {} double area_impl() const { return 3.14 * r * r; }};CRTP fournit un polymorphisme statique sans la surcharge des appels virtuels, ce qui est critique pour le C++ haute performance.
3. Principe de Substitution de Liskov (LSP)
Définition : Les objets d'une classe de base doivent pouvoir être remplacés par des objets de classes dérivées sans compromettre la correction du programme.
Une violation classique du LSP en C++ consiste à surcharger une méthode virtuelle en affaiblissant les préconditions ou en renforçant les postconditions. Considérons un exemple avec la hiérarchie « Rectangle — Carré ».
Antipattern
class Rectangle {protected:\