Composition : « L’acte de combiner des parties ou des éléments pour former un ensemble. » ~ Dictionary.com
Dans le premier cours de programmation, on nous a dit que le développement de logiciels est « le fait de décomposer un problème complexe en problèmes plus petits et de composer des solutions simples pour former une solution complète au problème complexe ».
On n’a jamais compris la signification de cette leçon au début. En fait, très peu de développeurs de logiciels ont une bonne compréhension de l’essence du développement logiciel. Ils ne sont pas conscients des outils les plus importants dont nous disposons, ni de la manière de les utiliser.
- Quelle est la composition de la fonction ?
- Quelle est la composition d’objet ?
Le problème est que vous ne pouvez pas éviter la composition juste parce que vous n’êtes pas au courant. Vous le faites toujours – mais vous le faites mal. Vous écrivez du code avec plus de bugs, et compliquez la tâche des autres développeurs. C’est un gros problème. Les effets sont très coûteux. Nous consacrons plus de temps à la maintenance des logiciels qu’à la création, et nos bugs ont un impact sur des milliards de personnes dans le monde entier.
Tout fonctionne avec un logiciel aujourd’hui. Chaque nouvelle voiture est un mini-ordinateur sur ses roues, et les problèmes de conception de logiciels causent de vrais accidents et coûtent de vraies vies humaines. En 2013, un jury a reconnu l‘équipe de développement de logiciels de Toyota coupable de « mépris insouciant » après qu’une enquête sur un accident eut révélé un code spaghetti avec 10 000 variables globales.
Les hackers et les gouvernements accumulent des bugs pour espionner les gens, voler des cartes de crédit, exploiter les ressources informatiques pour lancer des attaques DDoS (Distributed Denial of Service), pirater les mots de passe et même manipuler les élections.
Bref, nous devons faire de notre mieux dans la création des logiciels.
Composer des Fonctions
La composition de fonctions est le processus d’appliquer une fonction à la sortie d’une autre fonction. En algèbre, deux fonctions sont données : f et g, (f ∘ g) (x) = f (g (x)). Le cercle est l’opérateur de composition. Il est communément prononcé « composé avec » ou « après ». Vous pouvez dire cela à voix haute comme « f composé avec g est égal à f de g de x« , ou « f après g est égal à f de g de x« . On dit f après g parce que g est évalué en premier, alors sa sortie est passée en argument à f.
Chaque fois que vous écrivez un code comme celui-ci, vous composez des fonctions :
const g = n => n + 1; const f = n => n * 2;
const doStuff = x => { const afterG = g(x); const afterF = f(afterG); return afterF; };
doStuff(20); // 42
Chaque fois que vous écrivez une chaîne de promesses, vous composez des fonctions :
const g = n => n + 1; const f = n => n * 2;
const wait = time => new Promise( (resolve, reject) => setTimeout( resolve, time ) );
wait(300) .then(() => 20) .then(g) .then(f) .then(value => console.log(value)) // 42 ;
De même, à chaque fois que vous enchaînez des appels de méthode tableau, des méthodes lodash, des observables (RxJS, etc …), vous créez des fonctions. Si vous enchaînez, vous composez. Si vous transmettez des valeurs de retour dans d’autres fonctions, vous composez. Si vous appelez deux méthodes dans une séquence, vous composez en les utilisant comme données d’entrée.
Si vous enchaînez, vous composez.
Lorsque vous composez des fonctions intentionnellement, vous devrez le faire au mieux.
En composant des fonctions intentionnellement, nous pouvons améliorer notre fonction doStuff () en une simple ligne :
const g = n => n + 1; const f = n => n * 2;
const doStuffBetter = x => f(g(x));
doStuffBetter(20); // 42
Une objection commune à cette forme est qu’il est plus difficile à déboguer. Par exemple, comment pourrions-nous écrire cela en utilisant la composition des fonctions ?
const doStuff = x => { const afterG = g(x); console.log(`after g: ${ afterG }`); const afterF = f(afterG); console.log(`after f: ${ afterF }`); return afterF; };
doStuff(20); // => /* "after g: 21" "after f: 42" */
Tout d’abord, résumons que « après f », « après g » se connectent à un petit utilitaire appelé trace() :
const trace = label => value => { console.log(`${ label }: ${ value }`); return value; };
Maintenant, nous pouvons l’utiliser comme ceci :
const doStuff = x => { const afterG = g(x); trace('after g')(afterG); const afterF = f(afterG); trace('after f')(afterF); return afterF; };
doStuff(20); // => /* "after g: 21" "after f: 42" */
Les bibliothèques de programmation fonctionnelles populaires comme Lodash et Ramda incluent des utilitaires pour faciliter la composition des fonctions. Vous pouvez réécrire la fonction ci-dessus comme ceci :
import pipe from 'lodash/fp/flow';
const doStuffBetter = pipe( g, trace('after g'), f, trace('after f') );
doStuffBetter(20); // => /* "after g: 21" "after f: 42" */
Si vous voulez essayer ce code sans importer quelque chose, vous pouvez définir un tube (ou pipe) comme ceci :
// pipe(...fns: [...Function]) => x => y const pipe = (...fns) => x => fns.reduce((y, f) => f(y), x);
Ne vous inquiétez pas si vous ne comprenez pas comment cela fonctionne, encore. Plus tard, nous explorerons la composition des fonctions de manière beaucoup plus détaillée. En fait, c’est tellement essentiel, vous le verrez défini et démontré plusieurs fois tout au long de ce texte. Le but est de vous aider à devenir si familier que sa définition et son utilisation deviennent automatiques. Vous allez comprendre la composition.
pipe() crée un pipeline de fonctions, en passant la sortie d’une fonction à l’entrée d’une autre. Lorsque vous utilisez pipe () (et son twin, compose ()), vous n’avez pas besoin de variables intermédiaires. L’écriture de fonctions sans mention des arguments est appelée style sans point. Pour ce faire, vous appelez une fonction qui renvoie la nouvelle fonction, plutôt que de déclarer explicitement la fonction. Cela signifie que vous n’aurez pas besoin du mot-clé de la fonction ou de la syntaxe de la flèche (=>).
Le style sans point peut être pris trop loin, mais l’idéal c’est de le réduire, car ces variables intermédiaires ajoutent une complexité inutile à vos fonctions.
Il y a plusieurs avantages à la complexité réduite :
Mémoire de travail
Le cerveau humain moyen n’a que quelques ressources partagées pour des quanta discrets dans la mémoire de travail, et chaque variable consomme potentiellement l’un de ces quanta. À mesure que vous ajoutez d’autres variables, notre capacité à rappeler exactement la signification de chaque variable est diminuée. Les modèles de mémoire de travail impliquent typiquement 4-7 quanta discrets. Au-dessus de ces chiffres, les taux d’erreur augmentent considérablement.
En utilisant la forme de pipe, nous avons éliminé 3 variables – libérant presque la moitié de notre mémoire de travail disponible pour d’autres choses. Cela réduit considérablement notre charge cognitive. Les développeurs de logiciels ont tendance à mieux classer les données dans la mémoire de travail que la moyenne des personnes, mais pas tellement plus qu’à affaiblir l’importance de la conservation.
Rapport signal/bruit
Le code concis améliore également le rapport signal/bruit de votre code. C’est comme écouter une radio – quand la radio n’est pas correctement réglée sur la station, il y a beaucoup de bruit interférant et il est plus difficile d’entendre la musique. Lorsque vous syntonisez la station correcte, le bruit disparaît et vous obtenez un signal musical plus fort.
Et c’est pareil pour le code. Une expression de code plus concise conduit à une meilleure compréhension. Certains codes nous donnent des informations utiles, et d’autres prennent juste de la place. Si vous pouvez réduire la quantité de code que vous utilisez sans réduire le sens qui est transmis, vous rendrez le code plus facile à analyser et à comprendre pour les autres personnes qui ont besoin de le lire.
Surface pour les bugs
Jetez un coup d’œil aux fonctions avant et après. C’est comme si la fonction a été réduite et a perdu une tonne de poids. C’est important parce que le code supplémentaire signifie une surface supplémentaire pour cacher les bugs, ce qui signifie que plus de bugs s’y cacheront.
Moins de code = moins de surface pour les bugs = moins de bugs.
Composer des Objets
»Favoriser la composition des objets par rapport à l’héritage de classe » the Gang of Four, « Design Patterns : éléments de logiciels orientés objet réutilisables«
« En informatique, un type de données composite ou un type de données composé est un type de données qui peut être construit dans un programme en utilisant les types de données primitifs du langage de programmation et d’autres types composites. […] L’acte de construire un type composite est connu comme la composition. « ~ Wikipedia
Ce sont des primitives :
const firstName = 'Claude'; const lastName = 'Debussy';
Et ceci est un composite :
const fullName = { firstName, lastName };
De la même manière, tous les tableaux, ensembles, cartes, tableaux de bord, tableaux typés, etc … sont des types de données composites. Chaque fois que vous créez une structure de données non primitive, vous effectuez une sorte de composition d’objets.
Notez que le Gang of Four définit un pattern appelé pattern composite qui est un type spécifique de composition d’objet récursif. Ce dernier vous permet de traiter des composants individuels et des composites agrégés de manière identique. Certains développeurs sont confus, pensant que le pattern composite est la seule forme de composition d’objets. Ne soyez pas confus. Il existe plusieurs types de composition d’objets.
Le Gang of Four continue, « vous verrez la composition d’objets appliquée encore et encore dans les design patterns », puis ils cataloguent trois types de relations de composition d’objets, y compris la délégation (comme dans les patterns de l’état, de la stratégie et des visiteurs), la connaissance (lorsqu’un objet connaît un autre objet par référence, généralement passé en paramètre : une relation « uses-a », par exemple, un gestionnaire de requête réseau peut recevoir une référence à un logger pour consigner la requête – le gestionnaire de requête utilise un logger), et l’agrégation (lorsque les objets enfants font partie d’un objet parent : une relation « has-a », par exemple, les enfants DOM sont des éléments de composant dans un nœud DOM – un nœud DOM a des enfants).
L’héritage de classe peut être utilisé pour construire des objets composites, mais c’est une manière restrictive et fragile de le faire. Lorsque le Gang of Four dit « favoriser la composition d’objets par rapport à l’héritage de classe », ils vous conseillent d’utiliser des approches flexibles pour la construction d’objets composites plutôt que l’approche rigide et étroitement couplée de l’héritage de classe.
Nous utiliserons une définition plus générale de la composition d’objets de « Categorical Methods in Computer Science: With Aspects from Topology » (1989) :
« Les objets composites sont formés en mettant des objets ensemble de sorte que chacun de ces derniers fasse partie de l’ancien. »
Une autre bonne définition est dans le livre “Reliable Software Through Composite Design” de Glenford J Myers, en 1975. Les deux livres sont épuisés depuis longtemps, mais vous pouvez toujours trouver des vendeurs sur Amazon ou eBay si vous souhaitez explorer en profondeur la technique de la composition d’objets.
L’héritage de classe est juste un type de construction d’objet composite. Toutes les classes produisent des objets composites, mais tous les objets composites ne sont pas produits par les classes ou l’héritage de classe. « Favoriser la composition d’objets par rapport à l’héritage de classe » signifie que vous devez former des objets composites à partir de petites pièces, plutôt que d’hériter de toutes les propriétés d’un ancêtre dans une hiérarchie de classes. Ce dernier provoque une grande variété de problèmes bien connus dans la conception orientée objet :
- Problème de couplage étroit : Comme les classes enfants dépendent de l’implémentation de la classe parent, l’héritage de classe est le couplage le plus étroit disponible dans la conception orientée objet.
- Problème de classe de base fragile : en raison d’un couplage étroit, les modifications apportées à la classe de base peuvent potentiellement casser un grand nombre de classes descendantes, potentiellement dans le code géré par des tiers. L’auteur pourrait casser le code dont ils ne sont pas conscients.
- Problème inflexible de la hiérarchie : Avec les taxonomies d’ancêtres simples, avec suffisamment de temps et d’évolution, toutes les taxonomies de classe sont finalement erronées pour de nouveaux cas d’utilisation.
- Problème de la duplication par nécessité : Dû aux hiérarchies inflexibles, de nouveaux cas d’utilisation sont souvent mis en œuvre par duplication, plutôt que par extension, conduisant à des classes similaires qui sont de manière inattendue divergentes. Une fois que la duplication est établie, il n’est pas évident de savoir de quelle classe les nouvelles classes devraient descendre, ni pourquoi.
- Problème du gorille/banane : « … le problème avec les langages orientés objet est qu’ils ont tous cet environnement implicite qu’ils transportent avec eux. Vous vouliez une banane, mais ce que vous avez eu, c’était un gorille tenant la banane et toute la jungle. » : disait Joe Armstrong, dans » Coders at Work « .
La forme la plus courante de composition d’objets en JavaScript est connue sous le nom de concaténation d’objets (aussi connu comme composition mixin). Cela fonctionne comme de la glace. Vous commencez avec un objet (comme la glace à la vanille), puis mélangez les caractéristiques que vous voulez. Ajouter quelques noix, caramel, tourbillon de chocolat, et vous vous retrouvez avec de la crème glacée au chocolat et au caramel.
Construire des composites avec héritage de classe :
class Foo { constructor () { this.a = 'a' } }
class Bar extends Foo { constructor (options) { super(options); this.b = 'b' } }
const myBar = new Bar(); // {a: 'a', b: 'b'}
Construire des composites avec composition mixin :
const a = { a: 'a' };
const b = { b: 'b' };
const c = {...a, ...b}; // {a: 'a', b: 'b'}
Nous explorerons plus tard d’autres styles de composition d’objets. Pour l’instant, vous devriez comprendre que :
- Il y a plus d’une façon de le faire.
- Certaines manières sont meilleures que d’autres.
- Vous devez sélectionner la solution la plus simple et la plus flexible pour la tâche à accomplir.
Conclusion
Il ne s’agit pas de programmation fonctionnelle (FP) ou de programmation orientée objet (OOP), ni d’un langage par rapport à un autre. Les composants peuvent prendre la forme de fonctions, de structures de données, de classes, etc. Différents langages de programmation ont tendance à fournir différents éléments atomiques pour les composants. Java offre des classes, Haskell offre des fonctions, etc … Mais peu importe le langage et le paradigme que vous privilégiez, vous ne pouvez pas échapper à la création de fonctions et de structures de données. En fin de compte, c’est de cela dont il s’agit.
Nous parlerons beaucoup de la programmation fonctionnelle, car les fonctions sont les choses les plus simples à composer en JavaScript, et la communauté de la programmation fonctionnelle a investi beaucoup de temps et d’efforts pour formaliser les techniques de composition des fonctions.
Ce que nous ne ferons pas, c’est de dire que la programmation fonctionnelle est meilleure que la programmation orientée objet, ou que vous devez choisir l’une plutôt que l’autre. OOP vs FP est une fausse dichotomie. Toutes les vraies applications Javascript qu’on a vues ces dernières années mélangent largement FP et OOP.
Nous utiliserons la composition d’objets pour produire des types de données pour la programmation fonctionnelle, et la programmation fonctionnelle pour produire des objets pour la programmation orientée objet (OOP).
Peu importe comment vous écrivez un logiciel, vous devez le composer correctement.
L’essentiel du développement de logiciels est la composition.
Un développeur de logiciel qui ne comprend pas la composition est comme un constructeur de maisons qui ne connaît pas les boulons ou les clous. Construire un logiciel sans connaître la composition est comme un constructeur de maison mettant des murs ensemble avec du ruban adhésif et de la colle non appropriée.