Implémentation

Les classes

Cheat sheets : UML + DP + termes
Chaque composant sera usuellement implémenté par un ensemble de classes. Ce découpage des composants en classes reste une étape de conception, cependant elle commence à faire intervenir des notions d'implémentation.
Les classes doivent respecter les principes SOLID :

Single responsability

Comme pour les composants, une classe (ou une fonction) doit faire une seule chose et le faire bien. Cela facilite la lisibilité du code, son test, sa maintenance, sa réutilisation, son extension, etc.
Par exemple, une fonction calculant une somme et affichant le résultat a 2 responsabilités. Cela ne permettra pas de réutiliser le résultat ailleurs dans le code. Il est donc important de créer 2 fonctions ayant chacune sa propre responsabilité :
Autre exemple, un graphe se mettant à jour à partir d'une source de données. Il a 2 responsabilités : se dessiner et se mettre à jour. Il n'est ainsi pas possible de réutiliser (ou de tester) ce graphe sans le système de mise à jour (e.g. signaux, event, etc.). Si nos composants dépendent de ce système de synchronisation, toute modification de ce système impactera l'ensemble du projet. La synchronisation doit donc se faire à l'extérieur du graphe (DP servant), e.g. :
Un autre exemple, une classe peut faire beaucoup de choses, et ainsi avoir de nombreuses responsabilités. Pour éviter cela, on peut déléguer les différentes responsabilités dans plusieurs classes (e.g. , , etc.) qui seront utilisées par . Sa responsabilité sera alors de faire le lien (glue) entre les différents éléments du jeu. Cela réduira la taille de , rendra le code plus explicite, en facilitera la lecture, évitera certaines duplication de code, et permettra de tester les différents composants du jeu de manière indépendante.
Le fait de séparer les classes et les fonctions en composants élémentaires permettent leur réutilisation et évite la duplication de code. Imaginons une fonction , sauvegardant des données dans un fichier au format JSON. Cette fonction a 2 responsabilités : convertir les données au format JSON, puis les enregistrer dans un fichier. On peut cependant avoir plusieurs formats (JSON, XML, CSV, binaire, etc.). De même on peut avoir différents types de stockage (e.g. fichier, RAM, localStorage, indexDB, etc.). Il faudrait alors avoir F*S fonctions avec beaucoup de code dupliquées dans chacunes d'entre elles. Si on sépare cette fonction en 2, e.g. et , F+S fonctions suffisent alors pour supporter toutes les combinaisons possibles, e.g. .
Memento ?

Open-Close

Ce principe veut que le code permette l'ajout de nouvelles fonctionnalités, mais interdise la modification des classes/fonctions existantes. D'une part parce qu'on n'a pas toujours la main sur l'existant (e.g. bibliothèques), et d'autre part parce que toutes modifications d'une classe/fonction est source de bugs dans tout ce qui en dépend. Ce principe est ainsi nécessaire pour étendre le code tout en évitant de casser l'existant (regression), et ainsi garantissant la stabilité du code. Le but est aussi de laisser la porte aux modifications futures sans avoir à les anticiper (cf YAGNI).
Par exemple, si on souhaite pouvoir compresser nos données lors de leur enregistrement via , il nous faudra en modifier le code. Ce qui rajoutera une responsabilité à cette fonction, et en complexifiera le code. En revanche, si on a 2 fonctions et , nous n'avons pas besoin d'en modifier le code, et pouvons ajouter une simple fonction : . Le principe de responsabilité unique aide donc à respecter le principe Open-Close.
💡 Vous noterez que l'example ci-dessus peut très vite conduire à de longues lignes de code répétitives. Grâce à certains DP, vous pouvez créer une fonction e.g. , qui se chargera de tout cela pour vous.
Le premier réflexe est souvent d'utiliser l'héritage pour étendre une classe. Par exemple, des classes , , etc. héritant de . Cependant, dès que plusieurs extensions sont possibles, cela conduit très vite à une explosion du nombre de classes, e.g. , , etc. Il est ainsi souvent préférable d'utiliser une injection de dépendance, i.e. d'au lieu de définir un comportement par héritage, de le passer dans le constructeur de la classe mère (cf DP Strategy, Composite Reuse Principle (CRP) ):
Imaginons maintenant que nous souhaitions compresser le flux, le chiffrer, et/ou ajouter une somme de control. Une manière de le faire est d'utiliser un DP décorateur (cf mixins) pour définir de nouvelles méthodes, ou redéfinir des méthodes de :
DP Composite here ?
💡 Le Design Pattern Composite encapsule un ensemble d'éléments. Par exemple un groupe de TP est composé d'un ensemble d'étudiants, et fournit les moyennes du groupe. De même un groupe de TD est composé d'un ensemble de groupes de TP, et une promotion est composée d'un ensemble de groupe de TD.
⚠ L'implémentation n'a pas à représenter le réel, mais les données. Ainsi, même si une voiture rouge est une voiture, elle n'a pas forcément à être implémentée par un héritage (bien au contraire).

Liskov

- Liskov : (éviter bugs) contrat avant/après (pre/post guards closes - asserts), design by contract. Certains héritages pour spécialiser : une sorte d'héritage privé car respecte pas Liskov.
- exemples de problèmes - polymorphisme - spécialisation : héritage ou autres formes.

Interfaces

Les deux derniers principes sont assez simples. La ségrégation d'interface indique qu'un code doit dépendre d'une interface minimale, i.e. de ne dépendre que des fonctions dont il a besoin. Par exemple, une fonction qui lit un flux passé en paramètre doit pouvoir accepter un flux en lecture seule (i.e. qui n'a pas d'opération d'écriture). Dans certains langages, cela permet d'exploiter le duck typing.
L'inversion des dépendances indique qu'il faut dépendre d'une interface, et non d'une implémentation, permettant ainsi de changer l'implémentation utilisée, et évitant des dépendances circulaires dans certains langages. Cela permet de coder des algorithmes génériques et réutilisables. Par exemple, une fonction lisant un flux passé en paramètre a juste besoin que l'objet respecte une certaine interface, et n'a pas besoin d'une implémentation de flux particulière.

UML

- diagramme de classes + objet - diagramme de classe à la fois conception et durant implémentation. - POO - Objet : état à un instant T : débug ou montrer changement d'états. - État transition ?

Creationnal DP

- prototype (clone) + type obj - spawn - builder ~> ex gamectrler : plusieurs resps (encore !) - complex

Liste (TODO)

CM2/DOP CM2 abstraction ??? - Abstract factory (CM2 abstraction ???) - singleton -> Service locator (CM2???) - global scope. - factory method / virtual constructor (???) -> créer sans savoir réel type Structural - Adapter (CM2) - Bridge (CM2 avec adapter ?) - Facade (CM2) - Proxy (CM2) - Decorateur (CM2) - flyweight (DOP) Behavioral - Chain of resp = click handler (?) - Interpretor (NO) - Command (CM2) - peut aussi être callback + on peut passer param when call. - Iterator (CM2) - Mediator (CM2) - Observer (CM2) - Memento (CM2 ?) - State (CM3) vs Strategy (CM3) - Template method (CM2?) vs Behvioral ? - Visitor (?) - dble buffer (DOP) - game loop (CM2) - update method (?) + request update - type obj (CM3 - creationnal) - bytecode (NO) - service locator (CM3 - in place of singleton) - event queue (CM2) - object pool (DOP) - data locality (DOP) - dirty flag (DOP) - spacial separation (NO) - copy on write (p211, DOP) - instrumentalisé par un autre : servant DP (single resp ?)

Machines à État ???

- diagramme état-transition, DP state. + Diagramme de structure composite : quand la classe étudiée est complexe -> problème de design.

Qualité du code

En général, la lisibilité et maintenabilité du code importante bien plus que tout autre chose. En effet, il vaut souvent bien mieux un code lent, facilement modifiable, qu'un code inutilement rapide et complexe.
Pour cela il est bien évidemment nécessaire de bien indenter son code, d'avoir des noms explicites, une arborescence de fichiers explicite, etc. Un code bien écrit doit pouvoir se comprendre aisément en le survolant. On doit, par exemple, pouvoir comprendre l'usage et l'utilité d'une fonction à sa signature. Aussi, les fonctions doivent rester courtes (moins d'un écran), une fonction longue étant généralement le signe d'une responsabilité multiple ou d'un manque d'abstraction.
Il existe de multiples techniques et d'idiomes pour rendre un code plus explicite. Par exemple, gérer les cas d'erreurs en premier, utiliser des tableaux associatifs (de valeurs ou de fonctions), utiliser certains patrons de conceptions, pas de après un , , ou / etc.
Les commentaires ne doivent pas être redondant avec le code, i.e. ne doit pas décrire le code. Ce type de commentaires n'apportent pas d'informations utiles et peuvent très vite se retrouver obsolètes lorsque le code est modifié sans que les commentaires ne soient mis à jour, induisant alors les lecteurs en erreur. Les commentaires doivent rester rares et expliquer les nuances non-triviales du code, par exemple la correction d'un bug avec le lien de son ticket (e.g. github issue). De manière générale, si un code a besoin de commentaires pour être compris, il est généralement préférable de le réécrire pour le rendre plus explicite.
⚠ Il ne faut pas confondre les commentaires avec la documentation du code, qui sert à indiquer aux autres développeurs l'usage et les fonctionnalités d'une interface. Il est recommandé d'utiliser pour cela des outils qui généreront automatiquement la documentation à partir du code, et d'éventuels exemples d'usages (qui peuvent aussi servir de tests).
De manière générale, il ne faut pas se répéter (DRY - Do Not Repeat Yourself). En effet, il est alors très aisé de se retrouver avec des versions dupliquées différentes au fur et à mesure des modifications successives du code. Cela est aussi généralement peu confortable de fréquemment rechercher un bout de code pour le copier/coller à différents endroits. Lorsqu'un même bout de code est répété 3 fois, cela est généralement signe qu'il faut refactorer et factoriser le code.