Robert C. Martin – également connu sous le nom d’Oncle Bob – est un ingénieur logiciel américain bien connu. Il est l’auteur des livres à succès Nettoyer le code et Architecture épurée.
En 2000, il a écrit un article intitulé Principes de conception et modèles de conception où il a introduit quelques principes pour aider les développeurs à écrire du bon code dans la programmation orientée objet.
Ces principes visent à rendre les conceptions orientées objet plus compréhensibles, flexibles et maintenables. Ils facilitent la création de code lisible et testable avec lequel de nombreux développeurs peuvent travailler en collaboration n’importe où et n’importe quand. Et ils vous font prendre conscience de la droit façon d’écrire du code ????.
Tous les principes du document forment ensemble l’acronyme SOLID, qui a ensuite été introduit par Michel Plumes.
SOLID est un acronyme mnémotechnique qui représente les cinq principes de conception de la conception de classe orientée objet. Ces principes sont :
- S – Principe de responsabilité unique
- O – Principe ouvert-fermé
- L – Principe de substitution de Liskov
- je – Principe de ségrégation d’interface
- ré – Principe d’inversion de dépendance
Dans cet article, vous apprendrez ce que ces principes signifient et comment ils fonctionnent à l’aide d’exemples JavaScript. Les exemples devraient convenir même si vous ne maîtrisez pas parfaitement JavaScript, car ils s’appliquent également à d’autres langages de programmation.
Qu’est-ce que le principe de responsabilité unique (SRP) ?
Le principe de responsabilité unique, ou SRP, stipule qu’une classe ne devrait avoir qu’une seule raison de changer. Cela signifie qu’une classe ne doit avoir qu’un seul travail et faire une seule chose.
Prenons un exemple approprié. Vous serez toujours tenté de regrouper des classes similaires – mais malheureusement, cela va à l’encontre du principe de responsabilité unique. Pourquoi?
La ValidatePerson
l’objet ci-dessous a trois méthodes : deux méthodes de validation, (ValidateName()
et ValidateAge()
), et un Display()
méthode.
class ValidatePerson {
constructor(name, age) {
this.name = name;
this.age = age;
}
ValidateName(name) {
if (name.length > 3) {
return true;
} else {
return false;
}
}
ValidateAge(age) {
if (age > 18) {
return true;
} else {
return false;
}
}
Display() {
if (this.ValidateName(this.name) && this.ValidateAge(this.age)) {
console.log(`Name: ${this.name} and Age: ${this.age}`);
} else {
console.log('Invalid');
}
}
}
La Display()
La méthode va à l’encontre du SRP car le but est qu’une classe n’ait qu’un seul travail et ne fasse qu’une seule chose. La ValidatePerson
class effectue deux tâches : il valide le nom et l’âge de la personne, puis affiche certaines informations.
Le moyen d’éviter ce problème consiste à séparer le code qui prend en charge différentes actions et tâches afin que chaque classe n’exécute qu’une seule tâche et n’ait qu’une seule raison de changer.
Cela signifie que le ValidatePerson
class ne sera responsable que de la validation d’un utilisateur, comme indiqué ci-dessous :
class ValidatePerson {
constructor(name, age) {
this.name = name;
this.age = age;
}
ValidateName(name) {
if (name.length > 3) {
return true;
} else {
return false;
}
}
ValidateAge(age) {
if (age > 18) {
return true;
} else {
return false;
}
}
}
Alors que la nouvelle classe DisplayPerson
sera désormais chargé d’afficher une personne, comme vous pouvez le voir dans le bloc de code ci-dessous :
class DisplayPerson {
constructor(name, age) {
this.name = name;
this.age = age;
this.validate = new ValidatePerson(this.name, this.age);
}
Display() {
if (
this.validate.ValidateName(this.name) &&
this.validate.ValidateAge(this.age)
) {
console.log(`Name: ${this.name} and Age: ${this.age}`);
} else {
console.log('Invalid');
}
}
}
Avec cela, vous aurez rempli le principe de la responsabilité unique, ce qui signifie que nos classes n’ont plus qu’une seule raison de changer. Si vous voulez changer le DisplayPerson
classe, cela n’affectera pas la ValidatePerson
classer.
Qu’est-ce que le principe ouvert-fermé ?
Le principe ouvert-fermé peut prêter à confusion car il s’agit d’un principe à double sens. Selon de Bertrand Meyer définition sur Wikipédiala principe ouvert-fermé (OCP) stipule que les entités logicielles (classes, modules, fonctions, etc.) doivent être ouvertes pour extension, mais fermées pour modification.
Cette définition peut prêter à confusion, mais un exemple et des précisions supplémentaires vous aideront à comprendre.
Il existe deux attributs principaux dans l’OCP :
- Il est ouvert pour l’extension — Cela signifie que vous pouvez étendre ce que le module peut faire.
- Il est fermé for modification — Cela signifie que vous ne pouvez pas modifier le code source, même si vous pouvez étendre le comportement d’un module ou d’une entité.
OCP signifie qu’une classe, un module, une fonction et d’autres entités peuvent étendre leur comportement sans modifier leur code source. En d’autres termes, une entité doit être extensible sans modifier l’entité elle-même. Comment?
Par exemple, supposons que vous disposiez d’un tableau de iceCreamFlavours
, qui contient une liste de variantes possibles. Dans le makeIceCream
Classe A make()
La méthode vérifie si une saveur particulière existe et enregistre un message.
const iceCreamFlavors = ['chocolate', 'vanilla'];
class makeIceCream {
constructor(flavor) {
this.flavor = flavor;
}
make() {
if (iceCreamFlavors.indexOf(this.flavor) > -1) {
console.log('Great success. You now have ice cream.');
} else {
console.log('Epic fail. No ice cream for you.');
}
}
}
Le code ci-dessus ne respecte pas le principe OCP. Pourquoi? Eh bien, parce que le code ci-dessus n’est pas ouvert à une extension, ce qui signifie que vous pouvez ajouter de nouvelles saveurs, vous devez modifier directement le iceCreamFlavors
déployer. Cela signifie que le code n’est plus fermé à la modification. Haha (c’est beaucoup).
Pour résoudre ce problème, vous auriez besoin d’une classe ou d’une entité supplémentaire pour gérer l’ajout, vous n’avez donc plus besoin de modifier le code directement pour créer une extension.
const iceCreamFlavors = ['chocolate', 'vanilla'];
class makeIceCream {
constructor(flavor) {
this.flavor = flavor;
}
make() {
if (iceCreamFlavors.indexOf(this.flavor) > -1) {
console.log('Great success. You now have ice cream.');
} else {
console.log('Epic fail. No ice cream for you.');
}
}
}
class addIceCream {
constructor(flavor) {
this.flavor = flavor;
}
add() {
iceCreamFlavors.push(this.flavor);
}
}
Ici, nous avons ajouté une nouvelle classe — addIceCream
– pour gérer l’addition au iceCreamFlavors
tableau à l’aide de la add()
méthode. Cela signifie que votre code est fermé à la modification mais ouvert à une extension car vous pouvez ajouter de nouvelles saveurs sans affecter directement le tableau.
let addStrawberryFlavor = new addIceCream('strawberry');
addStrawberryFlavor.add();
makeStrawberryIceCream.make();
Notez également que SRP est en place car vous avez créé une nouvelle classe. ????
Qu’est-ce que le principe de substitution de Liskov ?
En 1987, le principe de substitution de Liskov (LSP) a été introduit par Barbara Liskov dans son discours de conférence « Abstraction des données ». Quelques années plus tard, elle en définit le principe ainsi :
« Soit Φ(x) une propriété prouvable sur les objets x de type T. Alors Φ(y) devrait être vrai pour les objets y de type S où S est un sous-type de T ».
Pour être honnête, cette définition n’est pas ce que de nombreux développeurs de logiciels veulent voir ???? – alors laissez-moi la décomposer en une définition liée à la POO.
Le principe définit que dans un héritage, les objets d’une superclasse (ou classe mère) doivent être substituables aux objets de ses sous-classes (ou classe enfant) sans casser l’application ni provoquer d’erreur.
En termes très simples, vous voulez que les objets de vos sous-classes se comportent de la même manière que les objets de votre super-classe.
Un exemple très courant est le scénario rectangle, carré. Il est clair que tous les carrés sont des rectangles car ce sont des quadrilatères dont les quatre angles sont des angles droits. Mais tous les rectangles ne sont pas des carrés. Pour être un carré, ses côtés doivent avoir la même longueur.
Gardant cela à l’esprit, supposons que vous ayez une classe rectangle pour calculer l’aire d’un rectangle et effectuer d’autres opérations comme set color :
class Rectangle {
setWidth(width) {
this.width = width;
}
setHeight(height) {
this.height = height;
}
setColor(color) {
// ...
}
getArea() {
return this.width * this.height;
}
}
Sachant parfaitement que tous les carrés sont des rectangles, vous pouvez hériter des propriétés du rectangle. Étant donné que la largeur et la hauteur doivent être identiques, vous pouvez l’ajuster :
class Square extends Rectangle {
setWidth(width) {
this.width = width;
this.height = width;
}
setHeight(height) {
this.width = height;
this.height = height;
}
}
En regardant l’exemple, cela devrait fonctionner correctement:
let rectangle = new Rectangle();
rectangle.setWidth(10);
rectangle.setHeight(5);
console.log(rectangle.getArea()); // 50
Dans ce qui précède, vous remarquerez qu’un rectangle est créé et que la largeur et la hauteur sont définies. Ensuite, vous pouvez calculer la zone correcte.
Mais selon le LSP, vous voulez que les objets de vos sous-classes se comportent de la même manière que les objets de votre super-classe. Cela signifie que si vous remplacez le Rectangle
avec Square
tout devrait encore bien fonctionner :
let square = new Square();
square.setWidth(10);
square.setHeight(5);
Vous devriez obtenir 100, car le setWidth(10)
est censé définir à la fois la largeur et la hauteur sur 10. Mais à cause de la setHeight(5)
cela renverra 25.
let square = new Square();
square.setWidth(10);
square.setHeight(5);
console.log(square.getArea()); // 25
Cela casse le LSP. Pour résoudre ce problème, il devrait y avoir une classe générale pour toutes les formes qui contiendra toutes les méthodes génériques auxquelles vous souhaitez que les objets de vos sous-classes aient accès. Ensuite, pour les méthodes individuelles, vous créez une classe individuelle pour le rectangle et le carré.
class Shape {
setColor(color) {
this.color = color;
}
getColor() {
return this.color;
}
}
class Rectangle extends Shape {
setWidth(width) {
this.width = width;
}
setHeight(height) {
this.height = height;
}
getArea() {
return this.width * this.height;
}
}
class Square extends Shape {
setSide(side) {
this.side = side;
}
getArea() {
return this.side * this.side;
}
}
De cette façon, vous pouvez définir la couleur et obtenir la couleur en utilisant les super ou les sous-classes :
// superclass
let shape = new Shape();
shape.setColor('red');
console.log(shape.getColor()); // red
// subclass
let rectangle = new Rectangle();
rectangle.setColor('red');
console.log(rectangle.getColor()); // red
// subclass
let square = new Square();
square.setColor('red');
console.log(square.getColor()); // red
Qu’est-ce que le principe de ségrégation d’interface ?
Le principe de ségrégation d’interface (ISP) stipule qu'”un client ne devrait jamais être contraint d’implémenter une interface qu’il n’utilise pas, ou que les clients ne devraient pas être contraints de dépendre de méthodes qu’ils n’utilisent pas”. Qu’est-ce que ça veut dire?
Tout comme le terme ségrégation le signifie, il s’agit de garder les choses séparées, c’est-à-dire de séparer les interfaces.
Noter: Par défaut, JavaScript n’a pas d’interfaces, mais ce principe s’applique toujours. Explorons donc cela comme si l’interface existait, afin que vous sachiez comment cela fonctionne pour d’autres langages de programmation comme Java.
Une interface typique contiendra des méthodes et des propriétés. Lorsque vous implémentez cette interface dans n’importe quelle classe, la classe doit définir toutes ses méthodes. Par exemple, supposons que vous disposiez d’une interface qui définit des méthodes pour dessiner des formes spécifiques.
interface ShapeInterface {
calculateArea();
calculateVolume();
}
Lorsqu’une classe implémente cette interface, toutes les méthodes doivent être définies même si vous ne les utiliserez pas ou si elles ne s’appliquent pas à cette classe.
class Square implements ShapeInterface {
calculateArea(){
//...
}
calculateVolume(){
//...
}
}
class Cuboid implements ShapeInterface {
calculateArea(){
//...
}
calculateVolume(){
//...
}
}
class Rectangle implements ShapeInterface {
calculateArea(){
//...
}
calculateVolume(){
//...
}
}
En regardant l’exemple ci-dessus, vous remarquerez que vous ne pouvez pas calculer le volume d’un carré ou d’un rectangle. Étant donné que la classe implémente l’interface, vous devez définir toutes les méthodes, même celles que vous n’utiliserez pas ou dont vous n’aurez pas besoin.
Pour résoudre ce problème, vous devez séparer l’interface.
interface ShapeInterface {
calculateArea();
calculateVolume();
}
interface ThreeDimensionalShapeInterface {
calculateArea();
calculateVolume();
}
Vous pouvez maintenant implémenter l’interface spécifique qui fonctionne avec chaque classe.
class Square implements ShapeInterface {
calculateArea(){
//...
}
}
class Cuboid implements ThreeDimensionalShapeInterface {
calculateArea(){
//...
}
calculateVolume(){
//...
}
}
class Rectangle implements ShapeInterface {
calculateArea(){
//...
}
}
Qu’est-ce que le principe d’inversion des dépendances ?
Ce principe vise à coupler de manière lâche les modules logiciels afin que les modules de haut niveau (qui fournissent une logique complexe) soient facilement réutilisables et non affectés par les modifications des modules de bas niveau (qui fournissent des fonctionnalités utilitaires).
Selon Wikipédiace principe stipule que :
- Les modules de haut niveau ne doivent rien importer des modules de bas niveau. Les deux doivent dépendre d’abstractions (par exemple, des interfaces).
- Les abstractions doivent être indépendantes des détails. Les détails (implémentations concrètes) doivent dépendre des abstractions.
En termes simples, ce principe stipule que vos classes doivent dépendre d’interfaces ou de classes abstraites au lieu de classes et de fonctions concrètes. Cela rend vos classes ouvertes à l’extension, selon le principe ouvert-fermé.
Prenons un exemple. Lors de la construction d’un magasin, vous voudriez que votre magasin utilise une passerelle de paiement comme Stripe ou tout autre mode de paiement préféré. Vous pourriez écrire votre code étroitement couplé à cette API sans penser à l’avenir.
Mais que se passe-t-il si vous découvrez une autre passerelle de paiement qui offre un bien meilleur service, disons PayPal ? Il devient alors difficile de passer de Stripe à Paypal, ce qui ne devrait pas être un problème de programmation et de conception de logiciels.
class Store {
constructor(user) {
this.stripe = new Stripe(user);
}
purchaseBook(quantity, price) {
this.stripe.makePayment(price * quantity);
}
purchaseCourse(quantity, price) {
this.stripe.makePayment(price * quantity);
}
}
class Stripe {
constructor(user) {
this.user = user;
}
makePayment(amountInDollars) {
console.log(`${this.user} made payment of ${amountInDollars}`);
}
}
En considérant l’exemple ci-dessus, vous remarquerez que si vous modifiez la passerelle de paiement, vous n’aurez pas seulement besoin d’ajouter la classe – vous devrez également apporter des modifications au Store
classer. Cela va non seulement à l’encontre du principe d’inversion de dépendance, mais également à l’encontre du principe ouvert-fermé.
Pour résoudre ce problème, vous devez vous assurer que vos classes dépendent d’interfaces ou de classes abstraites au lieu de classes et de fonctions concrètes. Pour cet exemple, cette interface contiendra tout le comportement que vous souhaitez que votre API ait et ne dépend de rien. Il sert d’intermédiaire entre les modules de haut niveau et de bas niveau.
class Store {
constructor(paymentProcessor) {
this.paymentProcessor = paymentProcessor;
}
purchaseBook(quantity, price) {
this.paymentProcessor.pay(quantity * price);
}
purchaseCourse(quantity, price) {
this.paymentProcessor.pay(quantity * price);
}
}
class StripePaymentProcessor {
constructor(user) {
this.stripe = new Stripe(user);
}
pay(amountInDollars) {
this.stripe.makePayment(amountInDollars);
}
}
class Stripe {
constructor(user) {
this.user = user;
}
makePayment(amountInDollars) {
console.log(`${this.user} made payment of ${amountInDollars}`);
}
}
let store = new Store(new StripePaymentProcessor('John Doe'));
store.purchaseBook(2, 10);
store.purchaseCourse(1, 15);
Dans le code ci-dessus, vous remarquerez que le StripePaymentProcessor
la classe est une interface entre Store
classe et la Stripe
classer. Dans une situation où vous devez utiliser PayPal, tout ce que vous avez à faire est de créer un PayPalPaymentProcessor
qui fonctionnerait avec le PayPal
classe, et tout fonctionnera sans affecter la Store
classer.
class Store {
constructor(paymentProcessor) {
this.paymentProcessor = paymentProcessor;
}
purchaseBook(quantity, price) {
this.paymentProcessor.pay(quantity * price);
}
purchaseCourse(quantity, price) {
this.paymentProcessor.pay(quantity * price);
}
}
class PayPalPaymentProcessor {
constructor(user) {
this.user = user;
this.paypal = new PayPal();
}
pay(amountInDollars) {
this.paypal.makePayment(this.user, amountInDollars);
}
}
class PayPal {
makePayment(user, amountInDollars) {
console.log(`${user} made payment of ${amountInDollars}`);
}
}
let store = new Store(new PayPalPaymentProcessor('John Doe'));
store.purchaseBook(2, 10);
store.purchaseCourse(1, 15);
Vous remarquerez également que cela suit le principe de substitution de Liskov car vous pouvez le remplacer par d’autres implémentations de la même interface sans casser votre application.
Ta-Da ????
Ça a été une aventure????. J’espère que vous avez remarqué que chacun de ces principes est lié aux autres d’une manière ou d’une autre.
Pour tenter de corriger un principe, disons le principe d’inversion de dépendance, vous vous assurez indirectement que vos classes sont ouvertes à l’extension mais fermées à la modification, par exemple.
Vous devez garder ces principes à l’esprit lorsque vous écrivez du code, car ils facilitent la collaboration de nombreuses personnes sur votre projet. Ils simplifient le processus d’extension, de modification, de test et de refactorisation de votre code. Assurez-vous donc de comprendre leurs définitions, ce qu’ils font et pourquoi vous en avez besoin au-delà de la POO.
Pour plus de compréhension, vous pouvez regarder cette vidéo par Beau Carnes sur le chaîne YouTube freeCodeCamp ou lisez cet article de Yiğit Kemal Erinç.
Amusez-vous à coder !