Table Des Matières
- 1 S.O.L.I.D Les 5 Premiers Principes De La Conception Orientée Objet Avec JavaScript
- 2 # Principe De Responsabilité Unique
- 3 # Principe Ouvert-Fermé
- 4 # Principe De Substitution Liskov
- 5 # Principe De Ségrégation De L’Interface
- 6 # Principe D’Inversion De Dépendance
- 7 # Code Complet
- 8 # Autres lectures et références
- 9 # Conclusion
S.O.L.I.D Les 5 Premiers Principes De La Conception Orientée Objet Avec JavaScript
J‘ai trouvé un très bon article expliquant les principes S.O.L.I.D. Si vous êtes familier avec PHP, vous pouvez lire l’article original (en anglais ) ici: S.O.L.I.D: Les 5 premiers principes de la conception orientée objet. Mais depuis que l’auteur originaire de ce post est un développeur JavaScript, il a dû adapter les exemples de code de cet article en JavaScript.
JavaScript est un langage faiblement typé, certains le considèrent comme un langage fonctionnel, d’autres le considèrent comme un langage orienté objet, certains pensent qu’il s’agit bien des deux, et certains pensent que c’est tout simplement une érreure le fait d’avoir des classes dans JavaScript. — Dor Tzur
Ceci est juste un simple article genre “Bienvenue S.O.L.I.D!”, il jette simplement la lumière sur ce que S.O.L.I.D. représente.
S.O.L.I.D est l’abréviation des cinq principes que nous allons traité un par un séparément à travers notre article
- S —Principe de responsabilité unique
- O —Principe ouvert férmé
- L —Principe de substitution de Liskov
- I — Principe de ségrégation de l’interface
- D —Principe de l’inversion des dépendances
# Principe De Responsabilité Unique
Une classe doit avoir une et une seule raison de changer, ce qui signifie qu’une classe ne devrait avoir qu’un seul travail.
Par exemple, disons que nous avons quelques formes et que nous voulions résumer toutes les zones de formes.. Eh bien, c’est assez simple non?
const circle = (radius) => { const proto = { type: 'Circle', //code } return Object.assign(Object.create(proto), {radius}) } const square = (length) => { const proto = { type: 'Square', //code } return Object.assign(Object.create(proto), {length}) }
D’abord, nous créons nos fonctions factory de formes et configurons les paramètres requis.
Qu’est-ce qu’une fonction Factory?
En JavaScript, n’importe quelle fonction peut renvoyer un nouvel objet. Quand ce n’est pas une fonction ou une classe constructeur, c’est ce qu’on appelle une fonction factory. pourquoi utiliser les fonctions factory, cet article fournit une bonne explication et cette vidéo l’explique aussi très clairement
Ensuite, nous continuons en créant la fonction factory areaCalculator, puis nous écrivons notre logique pour résumer la surface de toutes les formes fournies.
const areaCalculator = (s) => { const proto = { sum() { // logic to sum }, output () { return ` <h1> Sum of the areas of provided shapes: ${this.sum()} </h1> } } return Object.assign(Object.create(proto), {shapes: s}) }
Pour utiliser la fonction factory areaCalculator, nous appelons simplement la fonction et passons dans un tableau de formes, et affichons la sortie au bas de la page.
const shapes = [ circle(2), square(5), square(6) ] const areas = areaCalculator(shapes) console.log(areas.output())
Le problème avec la méthode de sortie est que areaCalculator gère la logique pour sortir les données. Par conséquent, que se passe-t-il si l’utilisateur veut sortir les données en tant que json ou autre chose?
Toute la logique serait gérée par la fonction factory areaCalculator , c’est ce que le ‘principe de responsabilité unique’ désapprouve; la fonction factory areaCalculator doit juste résumer les zones des formes fournies, il ne devrait pas se soucier si l’utilisateur veut JSON ou HTML.
Donc, pour résoudre ce problème, vous pouvez créer une fonction factory SumCalculatorOutputter et l’utiliser pour gérer toute la logique dont vous avez besoin sur la façon dont les zones de somme de toutes les formes fournies sont affichées.
La fonction factory SumCalculatorOutputter fonctionnerait comme ceci:
const shapes = [ circle(2), square(5), square(6) ] const areas = areaCalculator(shapes) const output = sumCalculatorOputter(areas) console.log(output.JSON()) console.log(output.HAML()) console.log(output.HTML()) console.log(output.JADE())
Maintenant, quelle que soit la logique dont vous avez besoin pour transmettre les données aux utilisateurs, elle est maintenant gérée par la fonction factory sumCalculatorOutputter.
# Principe Ouvert-Fermé
Les objets ou entités doivent être ouverts pour l’extension, mais fermés pour modification.
Ouvert pour extension signifie que nous devrions pouvoir ajouter de nouvelles fonctionnalités ou composants à l’application sans casser le code existant.
Fermé pour modification signifie que nous ne devrions pas introduire de changements brisants à la fonctionnalité existante parce que cela vous obligerait à refactoriser beaucoup de code existant – Eric Elliott
En termes plus simples, signifie qu’une fonction de classe ou d’usine dans notre cas, devrait être facilement extensible sans modifier la classe ou la fonction elle-même. Regardons la fonction factory areaCalculator, en particulier sa méthode sum.
sum () { const area = [] for (shape of this.shapes) { if (shape.type === 'Square') { area.push(Math.pow(shape.length, 2) } else if (shape.type === 'Circle') { area.push(Math.PI * Math.pow(shape.length, 2) } } return area.reduce((v, c) => c += v, 0) }
Si nous voulions que la méthode sum puisse additionner les zones de plus de formes, il faudrait ajouter plus de blocs if / else et cela va à l’encontre du principe open-closed.
Une façon dont nous pouvons améliorer cette méthode de somme consiste à supprimer la logique pour calculer la surface de chaque forme à partir de la méthode de somme et à la rattacher aux fonctions factory de la forme.
const square = (length) => { const proto = { type: 'Square', area () { return Math.pow(this.length, 2) } } return Object.assign(Object.create(proto), {length}) }
La même chose devrait être faite pour la fonction factory de cercle, une méthode de zone devrait être ajoutée. Maintenant, pour calculer la somme de toute forme fournie devrait être aussi simple que:
sum() { const area = [] for (shape of this.shapes) { area.push(shape.area()) } return area.reduce((v, c) => c += v, 0) }
Maintenant, nous pouvons créer une autre classe de forme et la transmettre lors du calcul de la somme sans casser notre code. Cependant, maintenant un autre problème se pose, comment savons-nous que l’objet passé dans la zone Calculatrice est en fait une forme ou si la forme a une méthode nommée zone?
Le codage d’une interface est une partie intégrante de S.O.L.I.D., un exemple rapide est que nous créons une interface, que chaque forme implémente.
Puisque JavaScript n’a pas d’interfaces, je vais vous montrer comment cela sera réalisé en TypeScript, puisque TypeScript modélise le POO classique pour JavaScript, et la différence avec JavaScript Prototypal OO pure.
interface ShapeInterface { area(): number } class Circle implements ShapeInterface { let radius: number = 0 constructor (r: number) { this.radius = r } public area(): number { return MATH.PI * MATH.pow(this.radius, 2) } }
L’exemple ci-dessus montre comment cela sera réalisé en TypeScript, mais sous le capot TypeScript compile le code en JavaScript pur et dans le code compilé il manque d’interfaces car JavaScript ne l’a pas.
Alors, comment pouvons-nous y parvenir, dans le manque d’interfaces?
Composition de la fonction aux secours!
Nous créons d’abord la fonction factory shapeInterface, car nous parlons d’interfaces, notre shapeInterface sera aussi abstraite qu’une interface, en utilisant la composition de la fonction, pour une explication approfondie de la composition voir cette superbe vidéo.
const shapeInterface = (state) => ({ type: 'shapeInterface', area: () => state.area(state) })
Ensuite, nous l’implémentons dans notre fonction factory carrée.
const square = (length) => { const proto = { length, type : 'Square', area : (args) => Math.pow(args.length, 2) } const basics = shapeInterface(proto) const composite = Object.assign({}, basics) return Object.assign(Object.create(composite), {length}) }
Et le résultat de l’appel de la fonction factory carrée sera le suivant:
const s = square(5) console.log('OBJ\n', s) console.log('PROTO\n', Object.getPrototypeOf(s)) s.area() // output OBJ { length: 5 } PROTO { type: 'shapeInterface', area: [Function: area] } 25
Dans notre zone Calculatrice sum méthode nous pouvons vérifier si les formes fournies sont réellement des types de forme Interface, sinon nous lançons une exception:
sum() { const area = [] for (shape of this.shapes) { if (Object.getPrototypeOf(shape).type === 'shapeInterface') { area.push(shape.area()) } else { throw new Error('this is not a shapeInterface object') } } return area.reduce((v, c) => c += v, 0) }
Et encore, puisque JavaScript ne supporte pas les interfaces comme les langages tapés, l’exemple ci-dessus montre comment nous pouvons le simuler, mais plus que simuler des interfaces, ce que nous faisons utilise des fermetures et la composition des fonctions. Si vous ne savez pas c’est quoi la zone de fermeture, cet article l’explique très bien et pour la complémentation, veuillez voir cette vidéo.
# Principe De Substitution Liskov
Soit q (x) une propriété prouvable sur les objets de x de type T. Alors q (y) devrait être prouvable pour les objets y de type S où S est un sous-type de T.
Tout ceci indique que chaque sous-classe / classe dérivée devrait être substituable à sa classe de base / parent.
En d’autres termes, aussi simple que cela, une sous-classe devrait remplacer les méthodes de la classe parente d’une manière qui ne brise pas la fonctionnalité du point de vue d’un client.
Toujours en utilisant notre fonction factory areaCalculator, disons que nous avons une fonction factory volumeCalculator qui étend la fonction factory areaCalculator, et dans notre cas pour étendre un objet sans casser les changements dans ES6 nous le faisons en utilisant Object.assign () et Object.getPrototypeOf():
const volumeCalculator = (s) => { const proto = { type: 'volumeCalculator' } const areaCalProto = Object.getPrototypeOf(areaCalculator()) const inherit = Object.assign({}, areaCalProto, proto) return Object.assign(Object.create(inherit), {shapes: s}) }
# Principe De Ségrégation De L’Interface
Un client ne devrait jamais être forcé d’implémenter une interface qu’il n’utilise pas ou les clients ne devraient pas être forcés de dépendre de méthodes qu’ils n’utilisent pas.
En continuant avec notre exemple de formes, nous savons que nous avons aussi des formes solides, donc comme nous voudrions aussi calculer le volume de la forme, nous pouvons ajouter un autre contrat à la shapeInterface:
const shapeInterface = (state) => ({ type: 'shapeInterface', area: () => state.area(state), volume: () => state.volume(state) })
Toute forme que nous créons doit implémenter la méthode du volume, mais nous savons que les carrés sont des formes plates et qu’ils n’ont pas de volumes, donc cette interface forcerait la fonction carrée à implémenter une méthode qu’elle n’utilise pas.
Le principe de ségrégation de l’interface n’acc-pte pas ceci, au lieu de cela vous pouvez créer une autre interface appelée solidShapeInterface qui a le volume de contrat et des formes solides comme des cubes etc.qui peuvent implémenter cette interface.
const shapeInterface = (state) => ({ type: 'shapeInterface', area: () => state.area(state) }) const solidShapeInterface = (state) => ({ type: 'solidShapeInterface', volume: () => state.volume(state) }) const cubo = (length) => { const proto = { length, type : 'Cubo', area : (args) => Math.pow(args.length, 2), volume : (args) => Math.pow(args.length, 3) } const basics = shapeInterface(proto) const complex = solidShapeInterface(proto) const composite = Object.assign({}, basics, complex) return Object.assign(Object.create(composite), {length}) }
C’est une approche qui est beaucoup mieux, mais il y a un piège à surveiller et c’est quand on veut calculer la somme pour la forme au lieu d’utiliser l’interface de forme ou une interface de forme solide.
Vous pouvez créer une autre interface, peut-être manageShapeInterface, et l’implémenter à la fois sur les formes plates et solides, c’est ainsi que vous pouvez facilement voir qu’il a une seule API pour gérer les formes, par exemple:
}) const circle = (radius) => { const proto = { radius, type: 'Circle', area: (args) => Math.PI * Math.pow(args.radius, 2) } const basics = shapeInterface(proto) const abstraccion = manageShapeInterface(() => basics.area()) const composite = Object.assign({}, basics, abstraccion) return Object.assign(Object.create(composite), {radius}) } const cubo = (length) => { const proto = { length, type : 'Cubo', area : (args) => Math.pow(args.length, 2), volume : (args) => Math.pow(args.length, 3) } const basics = shapeInterface(proto) const complex = solidShapeInterface(proto) const abstraccion = manageShapeInterface( () => basics.area() + complex.volume() ) const composite = Object.assign({}, basics, abstraccion) return Object.assign(Object.create(composite), {length}) }
Comme vous pouvez le voir jusqu’à présent, ce que nous avons fait pour les interfaces en JavaScript sont des fonctions factory pour la composition des fonctions.
Et ici, avec manageShapeInterface, ce que nous faisons est d’abstraire à nouveau la fonction de calcul, ce que nous faisons ici et dans les autres interfaces (si nous pouvons l’appeler interfaces), nous utilisons des fonctions de haut niveau pour réaliser les abstractions. Si vous ne savez pas c’est quoi une fonction d’ordre supérieur, vous pouvez voir cette vidéo.
# Principe D’Inversion De Dépendance
Les entités doivent dépendre d’abstractions et non de concrétions. elles indiquent que le module de haut niveau ne doit pas dépendre du module de bas niveau, mais qu’il doit dépendre d’abstractions.
En tant que langage dynamique, JavaScript ne nécessite pas l’utilisation d’abstractions pour faciliter le découplage. Par conséquent, la stipulation que les abstractions ne doivent pas dépendre des détails n’est pas particulièrement pertinente pour les applications JavaScript. La stipulation que les modules de haut niveau ne doivent pas dépendre de modules de bas niveau est cependant pertinente.
D’un point de vue fonctionnel, ces conteneurs et concepts d’injection peuvent être résolus avec une simple fonction d’ordre supérieur, ou un modèle de type hole-in-the-middle (trou-dans-le-milieu) qui sont intégrés directement dans le langage.
Comment l’inversion de dépendance est-elle liée aux fonctions d’ordre supérieur? est une question posée dans stackExchange si vous voulez une explication approfondie.
Cela peut sembler gonflé, mais c’est vraiment facile à comprendre. Ce principe permet le découplage.
Et nous l’avons déjà fait, passons en revue notre code avec manageShapeInterface et comment nous réalisons la méthode de calcul.
const manageShapeInterface = (fn) => ({ type: 'manageShapeInterface', calculate: () => fn() })
Ce que la fonction factory manageShapeInterface reçoit comme argument est une fonction d’ordre supérieur, qui découple pour chaque forme la fonctionnalité afin d’accomplir la logique nécessaire pour arriver au calcul final, voyons comment cela se fait dans les objets formes.
const square = (radius) => { // code const abstraccion = manageShapeInterface(() => basics.area()) // more code ... } const cubo = (length) => { // code const abstraccion = manageShapeInterface( () => basics.area() + complex.volume() ) // more code ... }
Pour le carré, ce que nous devons calculer, c’est juste obtenir la surface de la forme, et pour un cube, ce dont nous avons besoin est de sommer la surface avec le volume et c’est tout ce qu’il faut pour éviter le couplage et obtenir l’abstraction.
# Code Complet
Vous pouvez l’obtenir ici: solid.js
/* Code examples from the article: S.O.L.I.D The first 5 priciples of Object Oriented Design with JavaScript https://medium.com/@cramirez92/s-o-l-i-d-the-first-5-priciples-of-object-oriented-design-with-javascript-790f6ac9b9fa#.7uj4n7rsa */ const shapeInterface = (state) => ({ type: 'shapeInterface', area: () => state.area(state) }) const solidShapeInterface = (state) => ({ type: 'solidShapeInterface', volume: () => state.volume(state) }) const manageShapeInterface = (fn) => ({ type: 'manageShapeInterface', calculate: () => fn() }) const square = (length) => { const proto = { length, type : 'Square', area : (args) => Math.pow(args.length, 2) } const basics = shapeInterface(proto) const abstraccion = manageShapeInterface(() => basics.area()) const composite = Object.assign({}, basics, abstraccion) return Object.assign(Object.create(composite), {length}) } const circle = (radius) => { const proto = { radius, type: 'Circle', area: (args) => Math.PI * Math.pow(args.radius, 2) } const basics = shapeInterface(proto) const abstraccion = manageShapeInterface(() => basics.area()) const composite = Object.assign({}, basics, abstraccion) return Object.assign(Object.create(composite), {radius}) } const cubo = (length) => { const proto = { length, type : 'Cubo', area : (args) => Math.pow(args.length, 2), volume : (args) => Math.pow(args.length, 3) } const basics = shapeInterface(proto) const complex = solidShapeInterface(proto) const abstraccion = manageShapeInterface(() => basics.area() + complex.volume()) const composite = Object.assign({}, basics, abstraccion) return Object.assign(Object.create(composite), {length}) } const areaCalculator = (s) => { const proto = { type: 'areaCalculator', sum() { const area = [] for (shape of this.shapes) { area.push(shape.calculate()) } return area.reduce((v, c) => c += v, 0) } } return Object.assign(Object.create(proto), {shapes: s}) } const volumeCalculator = (s) => { const proto = { type: 'volumeCalculator' } const areaCalProto = Object.getPrototypeOf(areaCalculator()) const inherit = Object.assign({}, areaCalProto, proto) return Object.assign(Object.create(inherit), {shapes: s}) } const sumCalculatorOputter = (a) => { const proto = { JSON() { return JSON.stringify(this.calculator.sum()) }, HAML() { return `HAML format output` }, HTML() { return ` <h1> Sum of the areas of provided shapes: ${this.calculator.sum()} </h1>` }, JADE() { return `JADE format output` } } return Object.assign(Object.create(proto), {calculator: a}) } const shapes = [ circle(2), square(5), square(6) ] const solids = [ cubo(4) ] const areas = areaCalculator(shapes) const volume = volumeCalculator(solids) const output = sumCalculatorOputter(areas) const output2 = sumCalculatorOputter(volume) console.log(output.JSON()) console.log(output.HAML()) console.log(output.HTML()) console.log(output2.HTML()) console.log(output.JADE())
# Autres lectures et références
- SOLID les 5 premiers principes de OOD (En anglais SOLID the first 5 principles of OOD)
- 5 principes qui feront de vous un développeur JavaScript SOLID (En anglais 5 Principles that will make you a SOLID JavaScript )
- Série SOLID JavaScript (En anglais SOLID JavaScript Series)
- Principes SOLID à l’aide de Typescript (En anglais SOLID principles using Typescript)
# Conclusion
“Si vous prenez les principes SOLID à leurs extrêmes, vous arrivez à quelque chose qui rend la programmation fonctionnelle très attrayante” – Mark Seemann
JavaScript est un langage de programmation multi-paradigme, et nous pouvons lui appliquer les principes solides, et le plus important est que nous pouvons le combiner avec le paradigme de programmation fonctionnelle et obtenir le meilleur des deux mondes.
Javascript est aussi un langage de programmation dynamique, et très polyvalent
Ce que j’ai présenté est juste un moyen d’atteindre ces principes avec JavaScript, il peut y avoir d’autres options disant meilleures pour atteindre ces principes.
J’espère que vous avez apprécié ce post, je suis toujours en train d’explorer le monde JavaScript, donc je suis ouvert à accepter des commentaires ou des contributions, et si vous l’avez aimé, le recommander à un ami, le partager ou le relire.
Source: Article traduit en français depuis sa version originale en aglais “S.O.L.I.D The first 5 principles of Object Oriented Design with JavaScript” De son Auteur Cristian Ramirez