Les deux révisions précédentesRévision précédenteProchaine révision | Révision précédente |
java:coo [2020/04/24 11:15] – bruno | java:coo [2023/03/23 22:38] (Version actuelle) – bruno |
---|
===== Les piliers de la POO ===== | ===== Les piliers de la POO ===== |
La POO s'appuie sur quatre piliers essentiels: | La POO s'appuie sur quatre piliers essentiels: |
- **l'abstraction**: un modèle est une abstraction de la réalité, une classe est donc un modèle (la classe Chaise ,'est pas une chaise, mais un modèle de chaise). Cette abstraction peut être plus ou moins forte (déclaration d'un comportement attendu via une interface en java, classe abstraite, classe concrète). voir le sujet sur les modèles. | - **l'abstraction**: un modèle est une abstraction de la réalité, une classe est donc un modèle (la classe Chaise n'est pas une chaise, mais un modèle de chaise). Cette abstraction peut être plus ou moins forte (déclaration d'un comportement attendu via une interface en java, classe abstraite, classe concrète). voir le sujet sur les modèles. |
- **le polymorphisme**: capacité à présenter plusieurs forme, concerne les méthode mais également les substitutions. Il y a quatre principales manières d'utiliser le polymorphisme: | - **le polymorphisme**: capacité à présenter plusieurs forme, concerne les méthode mais également les substitutions. Il y a quatre principales manières d'utiliser le polymorphisme: |
- deux avec les méthodes exclusivement: la redéfinition (polymorphisme d'héritage) et la surcharge (polymorphisme paramétrique), lire [[https://web.maths.unsw.edu.au/~lafaye/CCM/poo/polymorp.htm|https://web.maths.unsw.edu.au/~lafaye/CCM/poo/polymorp.htm]]; | - deux avec les méthodes exclusivement: la redéfinition (polymorphisme d'héritage) et la surcharge (polymorphisme paramétrique), lire [[https://web.maths.unsw.edu.au/~lafaye/CCM/poo/polymorp.htm|https://web.maths.unsw.edu.au/~lafaye/CCM/poo/polymorp.htm]]; |
- une dernière développant l'idée de nommage commun du polymorphisme ad-hoc qui consiste à garantir la substitution d'objets, classiquement en java en utilisant une interface pour déclarer un comportement et en faisant implémenter cette interface par les classes concernées:{{ :java:principes:polym-avance.png?400 |Utilisation d'une interface Reparable avec une méthode reparer, implémentée par Chaise et Autoroute}} | - une dernière développant l'idée de nommage commun du polymorphisme ad-hoc qui consiste à garantir la substitution d'objets, classiquement en java en utilisant une interface pour déclarer un comportement et en faisant implémenter cette interface par les classes concernées:{{ :java:principes:polym-avance.png?400 |Utilisation d'une interface Reparable avec une méthode reparer, implémentée par Chaise et Autoroute}} |
- **l'héritage** ou la **spécialisation**: possibilité d'exprimer le fait qu'un concept (une classe) **est un** autre concept (plus exactement **est une** spécialisation d'une abstraction de plus haut niveau) et donc de bénéficier (hériter) de ce qui a été définit dans le concept parent;{{ :java:principes:heritage.png?100 |Le concept Chat est une spécialisation du concept Mammifère, lui même spécialisation de Animal }} | - **l'héritage** ou la **spécialisation**: possibilité d'exprimer le fait qu'un concept (une classe) **est un** autre concept (plus exactement **est une** spécialisation d'une abstraction de plus haut niveau) et donc de bénéficier (hériter) de ce qui a été définit dans le concept parent;{{ :java:principes:heritage.png?100 |Le concept Chat est une spécialisation du concept Mammifère, lui même spécialisation de Animal }} |
- **l'encapsulation**: offrir la possibilité à un programme de n'exposer qu'une partie du code qu'il utilise en masquant le fonctionnement interne d'un objet. Une première manière simple de garantir l'encapsulation est de restreindre la visibilité des attributs en les déclarant **private** et en utilisant des contrôles d'accès **lorsque cela est nécessaire** via les mutateurs (setters) et les accesseurs (getters); plus généralement, **//l'encapsulation ne se limite à la simple utilisation du mot-clef// private**, mais concerne tout ce qui contribue à masquer les fonctionnement interne et protéger les attributs de mutations par des tiers (par exemple, en clonant une référence dans un accesseur, ou en ne donnant pas les types concrets mais des super-types plus abstraits). | - **l'encapsulation**: offrir la possibilité à un programme de n'exposer qu'une partie du code qu'il utilise en masquant le fonctionnement interne d'un objet. Une première manière simple de garantir l'encapsulation est de restreindre la visibilité des attributs en les déclarant **private** et en utilisant des contrôles d'accès **lorsque cela est nécessaire** via les mutateurs (setters) et les accesseurs (getters); plus généralement, **//l'encapsulation ne se limite à la simple utilisation du mot-clef// private**, mais concerne tout ce qui contribue à masquer les fonctionnements internes et protéger les attributs de mutations par des tiers (par exemple, en clonant une référence dans un accesseur, ou en ne donnant pas les types concrets mais des super-types plus abstraits). |
| |
==== En résumé et guise de conclusion ==== | ==== En résumé et guise de conclusion ==== |
Je présente ici ceux qui font référence aujourd'hui. | Je présente ici ceux qui font référence aujourd'hui. |
| |
Ces principes s'appuient sur les quatre piliers présentés plus haut, et indique comment les utiliser correctement dans une conception objet. | Ces principes s'appuient sur les quatre piliers présentés plus haut, et indiquent comment les utiliser correctement dans une conception objet. |
==== Les principes SOLID ou SOIRS ==== | ==== Les principes SOLID ou SOIRS ==== |
SOLID est un acronyme proposé par Michael Feathers dans son ouvrage [[https://www.oreilly.com/library/view/working-effectively-with/0131177052/|Working effectively with legacy code]] qui reprend 4 principes compilés par Robert Martin dans son article de 2000 //{{ :java:principes:2000-martin-principles_and_patterns.pdf |Design Principles and Design Patterns}}// et un cinquième de 2002 extrait de Agile Software Development, Principles, Patterns, and Practices (au passage [[https://sites.google.com/site/unclebobconsultingllc/|le site de Martin]] regorge d'informations intéressantes). | SOLID est un acronyme proposé par Michael Feathers dans son ouvrage [[https://www.oreilly.com/library/view/working-effectively-with/0131177052/|Working effectively with legacy code]] qui reprend 4 principes compilés par Robert Martin dans son article de 2000 //{{ :java:principes:2000-martin-principles_and_patterns.pdf |Design Principles and Design Patterns}}// et un cinquième de 2002 extrait de Agile Software Development, Principles, Patterns, and Practices (au passage [[https://sites.google.com/site/unclebobconsultingllc/|le site de Martin]] regorge d'informations intéressantes). |
| |
Attention,// ce principe ne doit pas être confondu avec l'héritage//: l'héritage peut être une technique qui permet l'ouverture-fermeture, mais d'une part ce n'est pas toujours vrai (non respect du principe de Liskov par exemple) et d'autre part ce principe peut concerner le fonctionnement de plusieurs classes entre elles. L'héritage ne répondrait pas au problème dans ce cas. | Attention,// ce principe ne doit pas être confondu avec l'héritage//: l'héritage peut être une technique qui permet l'ouverture-fermeture, mais d'une part ce n'est pas toujours vrai (non respect du principe de Liskov par exemple) et d'autre part ce principe peut concerner le fonctionnement de plusieurs classes entre elles. L'héritage ne répondrait pas au problème dans ce cas. |
| |
| == Comment respecter ce principe? == |
| Ce principe est très général, il convient donc de l'avoir en tête lorsque l'on conçoit une architecture. |
| |
| Une première possibilité consiste à **//programmer des interfaces, pas des implémentations//**. Autrement dit, s'assurer que la modélisation dispose d'un niveau d'abstraction suffisant (utilisation d'interfaces en respectant le principe de ségrégation, éviter les objets dieu difficilement extensibles, garantir la substitution de Liskov, etc.). |
| |
| Ensuite, anticiper qu'une classe peut être dérivée: contraindre les héritiers à respecter le fonctionnement, exposer en public ou protected uniquement ce qui doit l'être, ne pas confier la gestion de ses attributs aux héritiers et plus généralement garantir l'encapsulation. |
| |
| Enfin et plus généralement, garder ce principe en tête lorsqu'on ajoute une responsabilité à une classe, une nouvelle fonctionnalité ou un nouvel attribut. Ne pas oublier non plus que ce principe s'applique à l'architecture dans tout ou partie de son ensemble, et pas uniquement à chaque classe individuellement. |
| |
| |
| === La substitution de Liskov === |
| == Description == |
| **//Une abstraction A doit pouvoir être substituée par n'importe laquelle de ses sous-abstractions sans que cela n'affecte le programme//** |
| |
| Autrement formulé, il s'agit lorsqu'on implémente des hiérarchies (extends) ou des comportements (implements en java) de s'assurer que la classe qui implémente réalise cette implémentation en conservant la philosophie et les principes de fonctionnement de la classe de plus haut niveau d'abstraction. |
| |
| Le fait d'exprimer un héritage ou une implémentation ne suffit donc pas à mettre en oeuvre le principe de substitution. Il s'agit bien de coder correctement la classe fille. |
| |
| Prenons par exemple le code suivant: |
| |
| <code java> |
| public class LiskovError{ |
| |
| private String message; |
| |
| public String toString(){ |
| String ret = "ok"; |
| |
| setMessage("Affichage de l'objet dans la console"); |
| System.out.println(super.toString()); |
| return ret; |
| } |
| |
| public void setMessage(String s){ |
| message = s; |
| } |
| } |
| </code> |
| |
| Quelles erreurs ont été commises? |
| |
| - la méthode toString() est censée renvoyer une chaîne de caractères décrivant l'instance. Ici, elle renvoie la chaîne "ok" uniquement. Si elle a des héritiers, ceux-ci ne pourront utiliser super.toString() sans enfreindre le comportement attentu de la méthode toString() tel que défini dans la classe Object |
| - la méthode toString() n'est pas une méthode d'affichage: en faisant des System.out.println, en utilisant setMessage, elle prend donc de nouvelles responsabilités qui ne relèvent pas de son contrat de fonctionnement. |
| |
| Une autre erreur fréquente vient d'une mauvaise modélisation d'une hiérarchie. L'exemple archi-classique est celui des formes géométriques (voir [[https://www.supinfo.com/articles/single/373-principe-substitution-liskov-lsp|https://www.supinfo.com/articles/single/373-principe-substitution-liskov-lsp]]). |
| |
| == Comment mettre en œuvre ce principe == |
| - En respectant les contrats définis par les interface ou classe de haut niveau. **D'où l'importance de la documentation**! Si vous ne savez pas ce qu'est censé faire une méthode, vous pouvez la redéfinir et lui donner un comportement aberrant! |
| - En utilisant des abstractions de haut niveau (interfaces en java, classes virtuelles pures, etc.) et en typant avec l'abstraction de plus haut niveau: vous pourrez ainsi substituer facilement une classe à une autre, pour peu que ces classes dérivent du type abstrait ET respectent le comportement défini dans l'abstraction. |
| |
| === La ségrégation des interfaces === |
| == Description == |
| Voici un très bon article très pédagogique: [[http://www.mechantblog.com/2014/02/solid-i-interface-segregation/|http://www.mechantblog.com/2014/02/solid-i-interface-segregation/]]. |
| |
| Pensez à la réutilisabilité du code: trop spécifique amène à impossible à réutiliser. |
| |
| Un bon concepteur se pose toujours la question de savoir s'il peut réutiliser un existant, et si ce n'est pas le cas comment rendre réutilisable ce qu'il doit développer. |
| |
| == Comment mettre en œuvre ce principe == |
| - en évitant les abstractions dieu |
| - en appliquant **aussi** le principe de responsabilité unique aux abstractions de haut niveau |
| - en n'hésitant pas à recourir à la délégation et à la composition |
| |
| === L'inversion de dépendances=== |
| == Description == |
| Les classes (abstractions) de haut niveau ne doivent pas dépendre des classes de bas niveau (implémentations). |
| |
| Ce principe illustre selon moi le mieux ce qu'est une conception objet. |
| |
| Pour bien comprendre la notion de dépendance, vous pouvez lire cet article: [[http://www.mechantblog.com/2014/05/solid-d-dependency-inversion/|http://www.mechantblog.com/2014/05/solid-d-dependency-inversion/]]. **Attention**, il n'est pas exhaustif sur le principe d'inversion de dépendance mais donne un bon exemple pour se représenter les choses. |
| |
| == Comment mettre en œuvre ce principe == |
| L'article précédent propose une manière simple d'obtenir une inversion de dépendance: passer par des abstractions de haut niveau. |
| |
| On retrouve ce principe dans le pattern Observer/Observé par exemple. |
| |
| Mais l'inversion de dépendance peut aller (beaucoup) plus loin. C'est vraiment l'idée de **ne pas se soucier des détails d'implémentation** qui prévaut. Autrement dit, si vous vous mettez en position de rendre vos classes de haut niveau fonctionnelles d'un point de vue logique, algorithmique et structurel, et que vous laissez bien le reste du travail à ceux qui feront vraiment le boulot (i.e. les classes concrètes), vous êtes dans de l'inversion de dépendance. |
| |
| Un bel exemple est le DP template method: la classe abstraite code un algo non modifiable (méthode final), et quand elle n'est pas en mesure de fournir une partie du code, elle externalise cette partie dans une méthode abstraite ou une interface que les héritiers auront en charge d'implémenter. |
| |
| En conclusion, il ne faut pas hésiter en objet à dégrossir le travail et indiquer les grande lignes directrices dans les abstractions, faire en sorte que ces abstractions soient facilement interopérables à haut niveau, et déléguer aux classes concrètes (héritiers, implémenteurs) la responsabilité de fournir le code spécifique sans pour autant modifier le comportement établi dans les classes de haut niveau. |
| |
| |
| |
| |