La réutilisation de code est un pratique consistant à utiliser du code existant plutôt que d'écrire du nouveau code. https://en.wikipedia.org/wiki/Code_reuse C'est en en général une façon de pratiquer l'heuristique [[DRY - Don’t repeat yourself]]. Il existe différentes manières de réutiliser du code [[Glossaire général#Encapsuler (programmation)|encapsulé]] à plusieurs niveaux: - [[Glossaire général#Variable|variable]] / [[Glossaire général#Constante|constante]] : réutiliser le résultat d'un calcul ou la valeur d'une expression - [[Glossaire général#Fonction|fonction]] / [[Glossaire général#Méthode|méthode]] : réutiliser une séquence d'instructions (paramétrable ou non) - [[Glossaire général#Objet|objet]] / [[Glossaire général#Classe|classe]] / [[Glossaire général#Type énuméré|enumération]] / [[Glossaire général#Record (java)|record]] : réutiliser un ensemble de données et de comportements cohérents - par [[Glossaire général#Polymorphisme|polymorphisme]] - par [[Glossaire général#Composition|composition]]/[[Glossaire général#Délégation|délégation]] - par [[Glossaire général#héritage de type|héritage de type]] - par [[Glossaire général#Héritage d’implémentation|héritage d'implémentation]] - à l'aide des [[Glossaire général#Type générique|types génériques]] - [[Glossaire général#Bibliothèque|bibliothèques]] / [[Glossaire général#Dépendance|dépendances]] : des modules de code écrits en interne ou par des [[Glossaire général#Tierce-partie (dépendance)|tierces-parties]]. En général on peut les importer à l'aide de [[Glossaire général#Gestionnaire de dépendances|gestionnaires de dépendances]] tels que [[Glossaire général#Maven|Maven]] ou [[Glossaire général#Gradle|gradle]] ## Délégation La délégation consiste à transférer un comportement à une autre classe. ```java public class A { private B b = new B(); public void methodA() { b.methodB(); } } ``` ### Délégation hybride Permet à une classe d'utiliser les fonctionnalités d'une autre classe sans utiliser d'héritage. En passant l'appelant en paramètre de la classe appelée. (ex. `b.methodB( this );`) ```java public class A { private B b = new B(); public void methodA() { b.methodB( this ); } } ``` > [!example]- Exemple > ```java > public class Carre implements Forme { > private final float cote; > private final String name; > private final AireFormatter formatter= new SquareCentimeterAireFormatter(); > > public Carre(float cote) { > this.cote = cote; > this.name = "Carré"; > } > > @Override > public float aire() { > return cote * cote; > } > > @Override > public String infos() { > // La responsabilité de formatter est déléguée à un autre objet > return formatter.infos(this,name); > } > } > ``` > > L'interface `AireFormatter` décrit la signature des méthodes des types qui la réalisent. > > ```java > public interface AireFormatter { > String infos(Forme forme, String label); > } > ``` > > Il est possible de changer la façon de formater l'aire en créant des implémentations différentes de l'interface `AireFormatter` > > ```java > public class SquareCentimeterAireFormatter implements AireFormatter { > @Override > public String infos(Forme forme, String name) { > return name + ", aire : " + (forme.aire() * 100) + "cm^2"; > } > } > ``` > > ```java > public class SquareMeterAireFormatter implements AireFormatter { > @Override > public String infos(Forme forme, String label) { > return label + ", aire : " + (forme.aire()) + "m^2"; > } > } > ``` ### Délégation par composition ou par aggrégation ? #### Composition Permet à une classe de définir des **comportements** et **attributs** de façon **modulaire**. L'objet `B` auquel on délègue un comportement est instancié par l'appelant `A`. L'instance de la classe `B` est détruite quand il n'existe plus de références de l'instance de la classe `A` . ```java public class A { private B b = new B(); public A() { } } ``` #### Aggrégation Permet à des instances de **réutiliser** des objets. > [!tip] L'aggrégation rend possible **l'injection de dépendances** ```java public class A { private B b; public A( B b ) { this.b = b; } } public class C { private B b = new B(); public C() { A a = new A( this.b ); } } ``` Une fois qu'il n'y a plus de références à une instance particulière de la classe `A`, son instance de classe `B` ne sera pas détruite. Dans cet exemple, `A` et `C` doivent être garbage collectés avant que `B` ne soit détruit. ## Réutiliser du code par héritage d'implémentation L’héritage d’implémentation permet de créer une nouvelle classe en **étendant** les **fonctionnalités** d’une classe existante. La classe qui est étendue est appelée **classe de base**, et la classe qui l’étend est appelée **classe dérivée**. La classe dérivée **hérite** de **l’implémentation** de la classe de base. L’héritage permet donc d’étendre une classe existante sans avoir à modifier son code. ### Classe abstraite Une classe abstraite porte le mot clé `abstract` et doit être héritée pour être instanciée. Une méthode abstraite porte le mot clé `abstract`. Elle n'est pas implémentée dans la classe abstraite mais doit l'être dans les classes qui en héritent. Elle permet de réutiliser du code pour éviter de le ré-écrire à chaque fois. C'est une sorte de modèle de classe à trou. ```java public abstract class Forme { private final String label; public Forme(String label) { this.label = label; } public abstract float aire(); // Ici, la classe abstraite peut s'appuyer sur des données // qui devront être fournies par les classes filles // Leur évitant de réimplémenter la logique ci-dessous // Elles n'auront qu'à fournir le nom de la forme au // constructeur de la classe de base // Les méthodes internes de la classe abstraite peuvent // également appeler les méthodes abstraites public String infos() { return this.label + ", aire : " + aire(); } } ``` Les classes dérivées doivent implémenter les méthodes abstraites. ```java public class Cercle extends Forme { private final float rayon; public Cercle(float rayon) { super("Cercle"); this.rayon = rayon; } @Override public float aire() { return (float) Math.PI * rayon * rayon; } } public class Carre extends Forme { private final float cote; public Carre(float cote) { super("Carré"); this.cote = cote; } @Override public float aire() { return cote * cote; } } ``` Mais la méthode `infos()` n'a pas besoin d'être ré-écrite. ```java Forme[] formes = {new Carre(2), new Cercle(2), new Carre(3)}; for (Forme forme:formes){ IO.println(forme.infos()); } ``` ## Héritage ou Composition/Délégation ? https://en.wikipedia.org/wiki/Composition_over_inheritance ### Avantages Favoriser la composition à l'héritage (d'implémentation) est un principe de conception qui donne à la conception **une plus grande flexibilité**. Il est plus naturel de construire des classes de domaine à partir de divers composants que d'essayer de trouver des points communs entre eux et d'en créer une hiérarchie. Par exemple, une pédale d'accélérateur et un volant partagent très peu de traits communs, mais les deux sont des composants essentiels dans une voiture. Ce qu'ils peuvent faire et comment ils peuvent être utilisés au profit de la voiture sont facilement définis. La composition offre également un domaine plus stable à long terme, car elle est moins sujette aux particularités singulières des membres de la famille. > [!tip] En d'autres termes, il est préférable de composer **ce qu'un objet peut faire** (has-a) que d'étendre **ce qu'il est** (is-a). La conception initiale est **simplifiée** en identifiant les comportements des objets système dans **des interfaces séparées** au lieu de créer une relation hiérarchique pour distribuer les comportements entre les classes de domaine via l'héritage. Cette approche s'adapte plus facilement aux futurs changements d'exigences qui nécessiteraient autrement une restructuration complète des classes d'une hierarchie dans le modèle d'héritage. De plus, il évite les problèmes souvent associés à des changements relativement mineurs à un modèle basé sur l'héritage qui comprend plusieurs parentés de classes. La relation de composition est plus flexible car elle peut être **modifiée au moment de l'exécution**, tandis que les relations de sous-typage sont **statiques** et doivent être recompilées dans de nombreux langages. ### Inconvénients Un inconvénient commun de l'utilisation de la composition au lieu de l'héritage est que les méthodes fournies par des composants individuels peuvent devoir être implémentées dans le type dérivé, même s'il ne s'agit que de méthodes de délégation En revanche, l'héritage ne nécessite pas que toutes les méthodes de la classe de base soient réimplémentées au sein de la classe dérivée. Au contraire, la classe dérivée n'a besoin que d'implémenter (override) les méthodes ayant un comportement différent des méthodes de classe de base. Cela peut nécessiter beaucoup moins d'efforts de programmation si la classe de base contient de nombreuses méthodes fournissant un comportement par défaut et que seules quelques-unes d'entre elles doivent être remplacées dans la classe dérivée. > [!tip] Certains inconvénients peuvent être mitigés à l'aide des méthodes par défaut dans les interfaces