Principes de Design
Dans cette unité de cours, nous allons examiner deux ensembles de lignes directrices de conception de haut niveau : GRASP et SOLID. GRASP fournit des directives plus abstraites et plus générales, tandis que SOLID cible des propriétés concrètes du code qui peuvent être facilement vérifiées. Ce qui est commun aux deux, c’est qu’ils fournissent des conseils pour la prise de décision sur la manière de structurer votre code en modules plus petits, notamment des classes.
Résumé de la leçon
Dans cette leçon, vous apprendrez une sélection de critères de conception qui vous aideront à structurer votre code pour respecter les principes de la POO.
GRASP
En 1997, Craig Larman a inventé le terme GRASP, qui signifie "General Responsibility Assignment Software Patterns" ( Patrons logiciels pour l’attribution générale de responsabilités). GRASP vise à être une aide générale à la prise de décision pour la conception des classes, c’est-à-dire des recommandations générales pour fournir des bases méthodiques, rationnelles et explicables pour la prise de décision.
Dans la première partie de cette leçon, nous passons en revue une sélection de patrons GRASP et illustrons leur signification à l’aide de visualisations et d’exemples de code.
Note terminologique
L’auteur de GRASP utilise le terme "Pattern" pour désigner les éléments de guidance individuels. Je n’apprécie pas particulièrement le terme "Pattern" car dans le contexte du développement orienté objet, ce terme se réfère presque toujours aux "Design Patterns", que nous étudierons intensivement dans une leçon à venir. Pour éviter toute confusion, je me référerai aux éléments GRASP comme des "principes" ou "directives", bien que dans la littérature courante, vous puissiez trouver le terme "patrons GRASP".
Expert en information
Le patron "Expert en information" (ou principe abrégé "Expert") consiste à définir une seule et unique classe responsable d’une tâche donnée.
Conformément au principe Expert en information, cette classe devrait contenir toutes les informations nécessaires pour
effectuer la tâche (expert).
Exemple
Dans une Bibliothèque, les frais de retard dépendent souvent du livre. Les livres populaires coûtent plus cher s’ils
sont rendus en retard. L’information est probablement stockée dans la classe Livre elle-même. Suivant cette logique,
c’est la classe Livre qui devrait calculer les frais de retard, et non une autre classe :
classDiagram
%% Classes
class Book {
+title: String
+author: String
+dailyLateFee: Float
+calculateLateFee(daysLate: int): Float
}
class LibraryMember {
+name: String
+memberId: String
}
class LibraryController {
+checkOutBook(book: Book, member: LibraryMember)
+returnBook(book: Book, member: LibraryMember)
}
%% Relationships
LibraryController --> Book : uses
LibraryController --> LibraryMember : uses
Créateur
Le principe "Créateur" vise à répondre à la question "Qui devrait être responsable de la création des objets d'une
classe donnée A ?" ou simplement : "Qui appelle new A ?"
Le principe Créateur guide en posant plusieurs questions. Un créateur raisonnable est toute autre classe qui fait déjà l'une des actions suivantes :
- Agrège des instances de
A - Contient des instances de
A - Enregistre des instances de
A - Utilise étroitement des instances de
A - Possède déjà les données nécessaires pour initialiser des instances de
A
Exemple : dans une plateforme de jeu en ligne, cela pourrait être un gestionnaire de session, qui suit les parties en cours et les parties sauvegardées.
Contrôleur
Attention : malgré le même nom, le principe Contrôleur de GRASP n'est pas lié au modèle MVC que nous avons vu dans la dernière unité de cours.
- Le principe "Contrôleur" indique quelle classe est responsable des événements à venir, par exemple "l’utilisateur a soumis un formulaire".
- Il est conseillé de regrouper les méthodes de gestion pour un cas d’utilisation commun dans une seule classe contrôleur.
- Mais attention : les classes contrôleur ne sont pas censées effectuer toutes les opérations lourdes elles-mêmes. Elles servent plutôt de coordinateurs, définissant des séquences d’actions consécutives, déléguées aux autres parties du système dans le bon ordre.
Le contrôleur doit déléguer
"[...] un contrôleur doit déléguer à d'autres objets le travail qui doit être fait ; il coordonne ou contrôle l'activité. Il ne fait pas beaucoup de travail lui-même."
-- Craig Larman - Applying UML Patterns
Exemple
La plupart des gestionnaires d'interface utilisateur entrent dans cette catégorie. Imaginez que vous avez un panneau avec de nombreux boutons : la classe recevant les événements d’interaction n’est généralement pas celle qui exécute les fonctionnalités déclenchées. Elle délègue simplement les appels vers la bonne destination.
Par exemple, si une interface utilisateur a un bouton pour trier une liste, le contrôleur délègue uniquement la responsabilité du tri, mais ne contient pas lui-même l’implémentation de l’algorithme de tri.
Forte cohésion
Métrique intra-classe : dans quelle mesure le contenu d’une classe est bien intégré.
Crédit image : Basé sur Wikipedia
Faible couplage
Couplage : Dans quelle mesure les modules logiciels (par ex. classes ou packages) sont interdépendants.
Idéalement, vous voulez un faible couplage, c’est-à-dire que les modules doivent être suffisamment autonomes pour remplir leur fonction sans se référer constamment à des modules externes.
Crédit image : Wikipedia
Pourquoi viser un couplage faible et non nul ?
Un couplage nul signifie "aucune connexion entre les modules". Il y a plusieurs modules, mais ils sont parfaitement isolés. Malheureusement, un couplage nul implique qu’il doit y avoir du code mort, car certains modules (c’est-à-dire des parties du code) ne sont jamais référencés.
Polymorphisme
Le polymorphisme fait référence à la pratique consistant à remplacer la prise de décision basée sur le type par la délégation à des objets à l’exécution.
Nous avons déjà discuté du polymorphisme dans une leçon précédente, donc ici une courte illustration de la structure de classe nécessaire pour un concert de canards polymorphiques :
classDiagram
class Duck {
<<Class>>
+String swim()
+String quack()
}
class RedheadDuck {
<<Class>>
}
class Mallard {
<<Class>>
}
Duck <|-- RedheadDuck : extends
Duck <|-- Mallard : extends
Variations protégées
Coder contre des interfaces est un exemple concret de ce principe : si le code de production ne fait aucune hypothèse sur la classe qui implémente une interface, cette classe peut être remplacée par une alternative sans conflit. En codant contre une interface plutôt qu’une implémentation concrète, l’appelant est protégé des variations dans la classe implémentante.
Avant
- Imaginons un jeu simple où un objet peut être déplacé dans une grille de 4x4.
- Les déplacements valides consistent à placer l’objet sur n’importe quelle case adjacente.
- Le
Modèleutilise en interne un tableau 2D de booléens pour représenter la position de l’objet. - Le contrôleur modifie la position de l’objet en accédant directement au tableau et en modifiant l’état.
public static void main(String[] args) {
// In this example the controller makes implementation assumptions on the model, and
// therefore hinders model implementation variations.
ModelWithoutVariationsPossible model = new ModelWithoutVariationsPossible();
System.out.println(model);
// Moving the item somewhere else requires directly messing with internals:
model.board[0][0] = false;
model.board[0][1] = true;
System.out.println(model);
}
Après
Non seulement il n’y a aucune protection de la cohérence (rien ne nous empêche d’avoir soudainement plusieurs objets),
mais nous ne pouvons pas non plus changer l’implémentation du Modèle sans modifier également le Contrôleur.
- Il suffirait de stocker la position de l’objet, plutôt que d’utiliser un tableau 2D de booléens, mais nous ne pouvons pas effectuer ce changement, car l’implémentation ne protège pas les variations.
- Une meilleure solution serait une interface simple, qui ne révèle rien sur les détails internes :
Nous pouvons maintenant facilement remplacer n’importe quelle implémentation par n’importe quelle variante :
classDiagram
%% Interface
class PositionModel {
<<interface>>
+moveUp()
+moveDown()
+moveLeft()
+moveRight()
}
%% Implementations
class TwoDimensionalArrayModelImpl {
+moveUp()
+moveDown()
+moveLeft()
+moveRight()
}
class PositionAsTwoIntValuesModelImpl {
+moveUp()
+moveDown()
+moveLeft()
+moveRight()
}
%% Controller depends on interface
class Controller {
+changePosition()
}
%% Relationships
TwoDimensionalArrayModelImpl ..|> PositionModel
PositionAsTwoIntValuesModelImpl ..|> PositionModel
Controller --> PositionModel : changePositionWithoutAssumingDetail
Quand coder contre une interface permet-il de protéger les variantes ?
Seulement lorsque l’interface cache les détails d’implémentation à un point tel que des implémentations alternatives sont possibles.
Fabrication pure
Lorsque aucune classe n’a de responsabilité naturelle, créez une classe artificielle pour la gérer.
Exemple : au lieu de faire gérer le stockage des fichiers par une classe Facture, créez une classe
FactureRepository, spécifiquement pour la tâche de persistance des factures.
La création de cette nouvelle classe "fabriquée" permet d’éviter d’encombrer Facture avec la logique de base de
données.
Avant
sequenceDiagram
participant Launcher
participant Invoice
Launcher->>Invoice: new Invoice(42, "Max", 123, [1,2,3])
activate Invoice
deactivate Invoice
Note right of Invoice: Constructor initializes fields
Launcher->>Invoice: write()
activate Invoice
Invoice-->>Launcher: write complete
deactivate Invoice
Le diagramme de séquence illustre une classe aux responsabilités mêlées (représentant à la fois les données et la persistance).
Après
sequenceDiagram
participant Launcher
participant Invoice
participant InvoiceFileSystemPersistor
Launcher->>Invoice: new Invoice(42, "Max", 123, [1,2,3])
activate Invoice
Note right of Invoice: Constructor initializes fields
deactivate Invoice
Launcher->>InvoiceFileSystemPersistor: new InvoiceFileSystemPersistor()
activate InvoiceFileSystemPersistor
Note right of InvoiceFileSystemPersistor: Creates persistor instance
deactivate InvoiceFileSystemPersistor
Launcher->>InvoiceFileSystemPersistor: write(i1)
activate InvoiceFileSystemPersistor
InvoiceFileSystemPersistor-->>Launcher: write complete
deactivate InvoiceFileSystemPersistor
Conception révisée : le diagramme de séquence montre que la représentation des données et la persistance sont gérées par des classes séparées.
Indirection
L’indirection vise à réduire le couplage fort entre les classes. L’idée est d’introduire un objet intermédiaire qui " découple" le couplage direct initial.
Exemple : nous gérons directement la base de données en utilisant une classe FactureRepository. On peut souligner que
l’appelant ne devrait pas se soucier de comment la persistance est réalisée exactement. L’utilisation d’une interface
de persistance générale permet de découpler le lien direct.
Avant
classDiagram
class Launcher {
+createInvoice() Invoice
}
class Invoice
class InvoiceFileSystemPersistor {
+write(invoice: Invoice)
}
Launcher --> Invoice : has
Launcher --> InvoiceFileSystemPersistor : uses
Après
classDiagram
direction TB
class Launcher {
+createInvoice() Invoice
+persistor: InvoicePersistor
}
class Invoice
class InvoicePersistor {
<<interface>>
+write(invoice: Invoice)
}
class InvoiceFileSystemPersistor {
+write(invoice: Invoice)
}
Launcher --> Invoice : has
Launcher --> InvoicePersistor : writes
InvoiceFileSystemPersistor ..|> InvoicePersistor : implements
SOLID
SOLID est un autre acronyme pour cinq principes fondamentaux de conception orientée objet. Contrairement à GRASP, chaque lettre représente un principe concret et applicable :
- S – Responsabilité unique
- O – Principe ouvert/fermé
- L – Substitution de Liskov
- I – Ségrégation des interfaces
- D – Inversion des dépendances
Nous allons examiner chacun de ces éléments ci-dessous.
Responsabilité unique
Essentiellement, le Principe de Responsabilité Unique (SRP) se résume à :
Concevez vos classes de manière à ce que chaque classe traite exactement un seul problème :
- Ne mêlez pas plusieurs responsabilités dans une même classe.
- Assurez-vous qu’aucune partie de la responsabilité de la classe n’est déléguée ailleurs.
- Assurez-vous que le "comment" une classe traite un problème est dissimulé.
Conseil de l’Oncle Bob
"Regroupez les choses qui changent pour les mêmes raisons. Séparez celles qui changent pour des raisons différentes."
Ainsi, en fin de compte, la responsabilité unique est un principe au niveau de la classe, prônant une forte cohésion et un faible couplage.
Voyez-vous un conflit entre le SRP et un principe GRASP ?
Expert en information : il stipule que nous devons mettre les calculs dans l’expert possédant les données nécessaires au calcul. Cela peut entrer en conflit avec le fait de donner à une classe un objectif unique (voir l’exemple du livre ci-dessus). Le principe qui prévaut dépend du contexte ; il peut être nécessaire de trouver un compromis.
Ouvert/Fermé
Le principe Ouvert/Fermé stipule que les modules logiciels (classes) doivent être :
- Ouverts à l’extension (autoriser les sous-classes lorsque des ajouts sont nécessaires)
- Fermés à la modification (ne pas modifier le code lorsque des ajouts sont nécessaires)
Illustration
Toute instruction switch sur un ensemble fixe d’options est un exemple de violation du principe Ouvert/Fermé, car
l’ajout d’une nouvelle option nécessite la modification de l’instruction switch existante :
class PaymentProcessor {
public void processPayment(String type) {
if (type.equals("credit")) {
// logic for credit card
} else if (type.equals("paypal")) {
// logic for PayPal
} else if (type.equals("crypto")) {
// logic for crypto
}
}
}
Quel est le problème ?
Nous ne pouvons pas ajouter un nouveau mode de paiement "or" sans modifier la cascade else/if ou switch, c’est-à-dire que le code est fermé à la modification. Le mode "or" ne peut pas non plus être ajouté en tant que sous-classe d’un mode de paiement existant, c’est-à-dire que les modes de paiement existants ne sont pas ouverts à l’extension (nécessaire).
Une solution plus propre serait d’utiliser une interface de paiement commune et des sous-classes pour les modes de paiement individuels :
classDiagram
class PaymentMethod {
<<interface>>
+pay()
}
class CreditCardPayment {
+pay()
}
class PayPalPayment {
+pay()
}
class PaymentProcessor {
+processPayment(PaymentMethod)
}
PaymentMethod <|.. CreditCardPayment
PaymentMethod <|.. PayPalPayment
PaymentProcessor --> PaymentMethod
L’ajout d’un autre mode de paiement, par exemple Crypto, serait parfaitement conforme au principe Ouvert/Fermé :
- Les modules existants ne sont pas modifiés : "Fermé" respecté.
- Les fonctionnalités supplémentaires sont obtenues par sous-classement : "Ouvert" appliqué.
Substitution de Liskov
Le principe de substitution de Liskov
Les méthodes qui utilisent des références aux classes de base doivent pouvoir utiliser des objets des classes dérivées sans le savoir.
Pour paraphraser, le principe de substitution de Liskov signifie : en termes de fonctionnalités, les sous-classes doivent fournir au moins tout ce que la classe parente fournit.
- Les sous-classes peuvent fournir des fonctionnalités supplémentaires.
- Les sous-classes ne doivent pas réduire les fonctionnalités.
Attention, Liskov inclut la compatibilité comportementale !
"Utiliser" dans la définition originale ne se limite pas à l’existence de méthodes (cela est déjà assuré par les règles de hiérarchie des classes en Java). Liskov inclut également la compatibilité comportementale, c’est-à-dire qu’une sous-classe ne doit pas différer dans le résultat, par exemple en lançant une exception là où la classe originale gérait l’erreur silencieusement.
Illustration
Le diagramme ci-dessous illustre le principe de substitution de Liskov:
classDiagram
class Bird {
+fly()
}
class Sparrow {
+fly()
+singSparrowSong()
}
Bird <|-- Sparrow
Surmonter les violations de principe
Nous allons maintenant étendre l’exemple précédent de Oiseau pour inclure une violation claire du principe de Liskov :
les manchots, bien qu’évidemment des Oiseaux, ne peuvent pas voler. Le diagramme de classes mettra donc en évidence
une fonctionnalité réduite de la sous-classe :
classDiagram
class Bird {
+fly()
}
class Sparrow {
+fly()
+singSparrowSong()
}
class Penguin {
+dive()
}
Bird <|-- Sparrow
Bird <|-- Penguin
Bien que sémantiquement correcte, notre classe Penguin n’adhère plus au principe de Liskov : un appelant qui invoque
Bird.fly() ne peut pas passer à la sous-classe Penguin sans remarquer une différence.
La solution de conception consiste à étendre la hiérarchie de classes, afin que nos oiseaux concrets (Sparrow,
Penguin) ne présentent que des changements fonctionnels supplémentaires par rapport à leurs superclasses respectives.
À quoi ressemblerait le diagramme de classes résultant ?
classDiagram
class Bird {
}
class FlyingBird {
+fly()
}
class FlightlessBird {
}
class Sparrow {
+fly()
+singSparrowSong()
}
class Penguin {
+dive()
}
Bird <|-- FlyingBird
Bird <|-- FlightlessBird
FlyingBird <|-- Sparrow
FlightlessBird <|-- Penguin
Ségrégation des interfaces
Le Principe de Ségrégation des Interfaces (ISP) vise à décomposer les grandes interfaces, notamment pour éviter de forcer l’implémentation de fonctions inutiles.
Illustration
Supposons que nous ayons une interface pour des imprimantes, offrant plusieurs fonctionnalités courantes (print,
scan, fax)
classDiagram
class Printer {
<<Interface>>
+print()
+scan()
+fax()
}
class OldPrinter {
+print()
}
class MultiFunctionPrinter {
+print()
+scan()
+fax()
}
Printer <|-- OldPrinter
Printer <|-- MultiFunctionPrinter
Le problème est le suivant : comme vous le savez, une classe qui implémente une interface doit implémenter toutes les méthodes de cette interface.
- Donc l’exemple ci-dessus n’est en fait pas possible :
OldPrinterdoit également implémenter les méthodesscanetfax— même si l’appareil ne supporte pas ces fonctions ! - On pourrait contourner le problème en introduisant des méthodes factices, qui lancent une exception lorsqu’elles sont appelées :
class OldPrinter implements Printer {
public void print() {
System.out.println("I might be old, but I can print !");
}
public void scan() {
throw new RuntimeException("Surprise, surprise: scan is actually not supported.");
}
public void fax() {
throw new RuntimeException("Surprise, surprise: fax is actually not supported.");
}
}
Donc, même si notre imprimante prétend offrir toutes les fonctionnalités de l’interface, elle ne le fait en réalité pas.
Qui est en faute ?
L’interface, parce qu’elle suppose trop de fonctionnalités communes pour toute imprimante. L’interface devrait être ségrégée pour mieux refléter la réalité.
Solution
La manière habituelle de résoudre ce problème est de séparer l’interface existante (souvent énorme) en plusieurs petites interfaces, chacune adaptée à un cas d’utilisation précis :
classDiagram
class Printer {
<<Interface>>
+print()
}
class Scanner {
<<Interface>>
+scan()
}
class Fax {
<<Interface>>
+fax()
}
class MultiFunctionPrinter {
+print()
+scan()
+fax()
}
class OldPrinter {
+print()
}
Printer <|-- MultiFunctionPrinter
Printer <|-- OldPrinter
Scanner <|-- MultiFunctionPrinter
Fax <|-- MultiFunctionPrinter
Qu’est-ce qui a changé ?
L’ancienne interface Printer (previous), qui offrait trois méthodes, a été divisée en trois interfaces distinctes pour des usages spécifiques : Fax, Scanner, Printer (new). Les appareils concrets n’implémentent que les interfaces correspondant réellement à leurs fonctionnalités. Plus de méthodes factices !
Inversion des dépendances
Les classes dépendent constamment d’autres classes. Une question souvent négligée est : Comment les classes obtiennent-elles leurs dépendances ?
Essentiellement, il existe deux options :
- Réclamer activement vos dépendances :
sequenceDiagram
participant Launcher
participant ClassicFlipper
participant ConsoleInputProvider
participant ConsoleOutputPrinter
%% Launcher creates ClassicFlipper
Launcher ->> ClassicFlipper: new ClassicFlipper()
activate ClassicFlipper
%% ClassicFlipper constructor creates dependencies
ClassicFlipper ->> ConsoleInputProvider: new ConsoleInputProvider()
activate ConsoleInputProvider
ConsoleInputProvider -->> ClassicFlipper: return
deactivate ConsoleInputProvider
ClassicFlipper ->> ConsoleOutputPrinter: new ConsoleOutputPrinter()
activate ConsoleOutputPrinter
ConsoleOutputPrinter -->> ClassicFlipper: return
deactivate ConsoleOutputPrinter
ClassicFlipper -->> Launcher: return
deactivate ClassicFlipper
%% Launcher calls flip method
Launcher ->> ClassicFlipper: flip()
activate ClassicFlipper
%% (Optional internal calls during flip can be added here)
ClassicFlipper -->> Launcher: return
deactivate ClassicFlipper
- Récévoir les dépendances :
sequenceDiagram
participant Launcher
participant IocFlipper
participant ConsoleInputProvider
participant ConsoleOutputPrinter
%% Launcher creates dependencies first
Launcher ->> ConsoleInputProvider: new ConsoleInputProvider()
activate ConsoleInputProvider
ConsoleInputProvider -->> Launcher: return
deactivate ConsoleInputProvider
Launcher ->> ConsoleOutputPrinter: new ConsoleOutputPrinter()
activate ConsoleOutputPrinter
ConsoleOutputPrinter -->> Launcher: return
deactivate ConsoleOutputPrinter
%% Launcher creates IocFlipper with injected dependencies
Launcher ->> IocFlipper: new IocFlipper(ConsoleInputProvider, ConsoleOutputPrinter)
activate IocFlipper
IocFlipper -->> Launcher: return
deactivate IocFlipper
%% Launcher calls flip method
Launcher ->> IocFlipper: flip()
activate IocFlipper
%% Optional: internal calls to input/output during flip could be shown here
IocFlipper -->> Launcher: return
deactivate IocFlipper
Réclamations actives de dépendances
- Intuitivement, on pourrait chercher à satisfaire toutes les dépendances d’une classe (ce dont elle a besoin pour fonctionner) dans son constructeur.
-
Par exemple, si une classe a pour but d’inverser des chaînes de caractères, elle a besoin de quelque chose pour recevoir les entrées et de quelque chose d’autre pour envoyer les sorties.
-
L’implémentation « naïve » consiste à satisfaire ces deux dépendances en réclamant activement les objets correspondants dans le constructeur :
package inversion; /** * The ClassicFlipper class obtains inputs from an InputProvider, and sends output to an OutputPrinter. */ class ClassicFlipper implements Flipper { // Two dependencies (we don't care how they function internally) private InputProvider inputProvider; private OutputPrinter outputPrinter; /** * Default constructor, has no arguments. The class is in control: The class saturates its * dependencies by itself creating the corresponding objects. */ public ClassicFlipper() { // We're in control, we actively saturate our dependencies: this.inputProvider = new ConsoleInputProvider(); this.outputPrinter = new ConsoleOutputPrinter(); } /** * A ClassicFlipper method. Required something providing input and something the output can be sent to * (the two object dependencies satisfy this need). */ public void flip() { String input = inputProvider.getInput(); String flipped = new StringBuilder(input).reverse().toString(); outputPrinter.printOutput(flipped); } } -
The main class does not pass any arguments to
Flipper:
-
Cette approche pose plusieurs problèmes :
- L’implémentation dépend d’instances concrètes de ses dépendances.
- Il est impossible de tester la classe isolément.
- Le remplacement d’une dépendance par une implémentation alternative oblige à modifier l’implémentation de
Flipper.
Recevoir les dépendances
L’alternative via l’inversion de contrôle consiste à…
- éliminer toutes les réclamations actives de dépendances dans l’implémentation de
Flipper - recevoir plutôt les dépendances comme paramètres de constructeur
/**
* Inversion of Control variant of ClassicFlipper. Receives all dependencies via constructor. Only
* depends on interfaces.
*/
public class IocFlipper implements Flipper {
// Two dependencies (we don't care how they function internally)
private InputProvider inputProvider;
private OutputPrinter outputPrinter;
/**
* Default constructor, has no arguments. The class is in control: The class saturates its
* dependencies by itself creating the corresponding objects.
*/
public IocFlipper(InputProvider inputProvider, OutputPrinter outputProvider) {
// We're in control, we actively saturate our dependencies:
this.inputProvider = inputProvider;
this.outputPrinter = outputProvider;
}
/**
* A ClassicFlipper method. Required something providing input and something the output can be
* sent to (the two object dependencies satisfy this need).
*/
public void flip() {
String input = inputProvider.getInput();
String flipped = new StringBuilder(input).reverse().toString();
outputPrinter.printOutput(flipped);
}
}
La classe principale (main) injecte maintenant toutes les dépendances requises via le constructeur de Flipper :
void main() {
// IoC flipper, with parameter argument injection
// IocFlipper is coded against interface, will accept any InputProvider / OutputPrinter
Flipper iocFlipper = new IocFlipper(new ConsoleInputProvider(), new ConsoleOutputPrinter());
iocFlipper.flip();
}
Avantages :
Flipperne dépend que d’interfaces : les classes concrètes peuvent être remplacées facilement.Flipperpeut être testé isolément (en passant des mocks).
Perspective
Pour être complet
Techniquement, il existe une troisième possibilité : l’autowiring. Cela consiste à utiliser des annotations et de la réflexion pour saturer « magiquement » toutes les dépendances, même si elles ne sont ni réclamées ni demandées. Certains frameworks fortement basés sur la réflexion, comme Spring, imposent même l’autowiring comme méthode principale d’injection, ce qui peut être déroutant pour les débutants. L’autowiring est un sujet à part entière qui dépasse la portée de ce cours. Pour les curieux, vous pouvez poursuivre votre lecture ici (Chapitre 2).
Littérature
Inspiration et lectures complémentaires pour les esprits curieux :