9.1 Inversion de dépendance

Rappelez-vous, nous avons dit un jour que dans une application serveur, vous ne pouvez pas simplement créer des flux via new Thread().start()? Seul le conteneur doit créer des threads. Nous allons maintenant développer encore plus cette idée.

Tous les objets doivent également être créés uniquement par le conteneur . Bien sûr, nous ne parlons pas de tous les objets, mais plutôt des objets dits métiers. Ils sont aussi souvent appelés poubelles. Les jambes de cette approche se développent à partir du cinquième principe de SOLID, qui nécessite de se débarrasser des classes et de passer aux interfaces :

  • Les modules de niveau supérieur ne doivent pas dépendre des modules de niveau inférieur. Ceux-ci et d'autres devraient dépendre d'abstractions.
  • Les abstractions ne doivent pas dépendre des détails. L'implémentation doit dépendre de l'abstraction.

Les modules ne doivent pas contenir de références à des implémentations spécifiques, et toutes les dépendances et interactions entre eux doivent être construites uniquement sur la base d'abstractions (c'est-à-dire d'interfaces). L'essence même de cette règle peut être écrite en une phrase : toutes les dépendances doivent être sous la forme d'interfaces .

Malgré sa nature fondamentale et sa simplicité apparente, cette règle est le plus souvent violée. À savoir, chaque fois que nous utilisons le nouvel opérateur dans le code du programme/module et créons un nouvel objet d'un type spécifique, ainsi, au lieu de dépendre de l'interface, la dépendance à l'implémentation est formée.

Il est clair que cela ne peut être évité et que les objets doivent être créés quelque part. Mais, à tout le moins, vous devez minimiser le nombre d'endroits où cela est fait et dans lesquels les classes sont explicitement spécifiées, ainsi que localiser et isoler ces endroits afin qu'ils ne soient pas dispersés dans le code du programme.

Une très bonne solution est l'idée folle de concentrer la création de nouveaux objets au sein d'objets et de modules spécialisés - usines, localisateurs de services, conteneurs IoC.

Dans un sens, une telle décision suit le principe du choix unique, qui dit : "Chaque fois qu'un système logiciel doit prendre en charge de nombreuses alternatives, leur liste complète ne doit être connue que d'un seul module du système" .

Par conséquent, si à l'avenir il est nécessaire d'ajouter de nouvelles options (ou de nouvelles implémentations, comme dans le cas de la création de nouveaux objets que nous envisageons), alors il suffira de mettre à jour uniquement le module qui contient ces informations, et tous les autres modules ne seront pas affectés et pourront continuer leur travail comme d'habitude.

Exemple 1

new ArrayList Au lieu d' écrire quelque chose comme , il serait logique List.new()que le JDK vous fournisse l'implémentation correcte d'une feuille : ArrayList, LinkedList ou même ConcurrentList.

Par exemple, le compilateur voit qu'il y a des appels à l'objet à partir de différents threads et y met une implémentation thread-safe. Ou trop d'insertions au milieu de la feuille, alors l'implémentation sera basée sur LinkedList.

Exemple 2

Cela s'est déjà produit avec les sortes, par exemple. À quand remonte la dernière fois que vous avez écrit un algorithme de tri pour trier une collection ? Au lieu de cela, tout le monde utilise maintenant la méthode Collections.sort(), et les éléments de la collection doivent prendre en charge l'interface Comparable (comparable).

Si sort()vous passez une collection de moins de 10 éléments à la méthode, il est tout à fait possible de la trier avec un tri à bulles (Bubble sort), et non Quicksort.

Exemple 3

Le compilateur regarde déjà comment vous concaténez les chaînes et remplacera votre code par StringBuilder.append().

9.2 Inversion de dépendance en pratique

Maintenant le plus intéressant : réfléchissons à la façon dont nous pouvons combiner théorie et pratique. Comment les modules peuvent-ils correctement créer et recevoir leurs "dépendances" et ne pas violer l'inversion de dépendance ?

Pour ce faire, lors de la conception d'un module, vous devez décider vous-même :

  • ce que fait le module, quelle fonction il remplit ;
  • ensuite le module a besoin de son environnement, c'est-à-dire quels objets/modules il devra traiter ;
  • Et comment va-t-il l'obtenir ?

Pour se conformer aux principes de l'inversion de dépendance, vous devez absolument décider quels objets externes votre module utilise et comment il obtiendra des références à ceux-ci.

Et voici les options suivantes :

  • le module lui-même crée des objets ;
  • le module prend des objets du conteneur ;
  • le module n'a aucune idée d'où viennent les objets.

Le problème est que pour créer un objet, vous devez appeler un constructeur d'un type spécifique, et par conséquent, le module ne dépendra pas de l'interface, mais de l'implémentation spécifique. Mais si nous ne voulons pas que les objets soient créés explicitement dans le code du module, nous pouvons utiliser le modèle Factory Method .

"L'essentiel est qu'au lieu d'instancier directement un objet via new, nous fournissons à la classe client une interface pour créer des objets. Puisqu'une telle interface peut toujours être remplacée par la bonne conception, nous obtenons une certaine flexibilité lors de l'utilisation de modules de bas niveau dans les modules de haut niveau" .

Dans les cas où il est nécessaire de créer des groupes ou des familles d'objets liés, une fabrique abstraite est utilisée à la place d'une méthode de fabrique .

9.3 Utilisation du localisateur de service

Le module prend les objets nécessaires à celui qui les possède déjà. On suppose que le système dispose d'un référentiel d'objets, dans lequel les modules peuvent "mettre" leurs objets et "prendre" des objets du référentiel.

Cette approche est implémentée par le modèle Service Locator , dont l'idée principale est que le programme a un objet qui sait comment obtenir toutes les dépendances (services) qui peuvent être nécessaires.

La principale différence avec les usines est que Service Locator ne crée pas d'objets, mais contient en fait déjà des objets instanciés (ou sait où / comment les obtenir, et s'il les crée, alors une seule fois lors du premier appel). L'usine à chaque appel crée un nouvel objet dont vous obtenez la pleine propriété et vous pouvez en faire ce que vous voulez.

Important ! Le localisateur de service produit des références aux mêmes objets déjà existants . Par conséquent, vous devez être très prudent avec les objets émis par le Service Locator, car quelqu'un d'autre peut les utiliser en même temps que vous.

Les objets dans le localisateur de service peuvent être ajoutés directement via le fichier de configuration, et en fait de n'importe quelle manière pratique pour le programmeur. Le localisateur de service lui-même peut être une classe statique avec un ensemble de méthodes statiques, un singleton ou une interface, et peut être transmis aux classes requises via un constructeur ou une méthode.

Le localisateur de service est parfois appelé un anti-modèle et est déconseillé (car il crée des connexions implicites et ne donne que l'apparence d'une bonne conception). Vous pouvez lire plus de Mark Seaman :

9.4 Injection de dépendance

Le module ne se soucie pas du tout des dépendances "minières". Il détermine uniquement ce dont il a besoin pour fonctionner, et toutes les dépendances nécessaires sont fournies (introduites) de l'extérieur par quelqu'un d'autre.

C'est ce qu'on appelle - l'injection de dépendance . En règle générale, les dépendances requises sont transmises soit en tant que paramètres de constructeur (Constructor Injection), soit via des méthodes de classe (Setter injection).

Cette approche inverse le processus de création de dépendances - au lieu du module lui-même, la création de dépendances est contrôlée par quelqu'un de l'extérieur. Le module de l'émetteur actif d'objets devient passif - ce n'est pas lui qui crée, mais d'autres créent pour lui.

Ce changement de direction s'appelle l'inversion du contrôle ou le principe d'Hollywood - "Ne nous appelez pas, nous vous appellerons."

C'est la solution la plus flexible, donnant aux modules la plus grande autonomie . Nous pouvons dire que seul il met pleinement en œuvre le "principe de responsabilité unique" - le module doit être entièrement concentré sur le fait de bien faire son travail et de ne se soucier de rien d'autre.

Fournir au module tout le nécessaire pour le travail est une tâche distincte, qui doit être gérée par le "spécialiste" approprié (généralement un certain conteneur, un conteneur IoC, est responsable de la gestion des dépendances et de leur mise en œuvre).

En fait, tout ici est comme dans la vie : dans une entreprise bien organisée, les programmeurs programment, et les bureaux, les ordinateurs et tout ce dont ils ont besoin pour travailler sont achetés et fournis par le chef de bureau. Ou, si vous utilisez la métaphore du programme en tant que constructeur, le module ne devrait pas penser aux fils, quelqu'un d'autre est impliqué dans l'assemblage du constructeur, et non les pièces elles-mêmes.

Il ne serait pas exagéré de dire que l'utilisation d'interfaces pour décrire les dépendances entre modules (Dependency Inversion) + la création et l'injection correctes de ces dépendances (principalement Dependency Injection) sont des techniques clés pour le découplage .

Ils servent de socle sur lequel repose le couplage lâche du code, sa flexibilité, sa résistance aux changements, à la réutilisation, et sans lequel toutes les autres techniques n'ont guère de sens. C'est la base d'un couplage lâche et d'une bonne architecture.

Le principe de l'inversion de contrôle (avec l'injection de dépendance et le localisateur de service) est discuté en détail par Martin Fowler. Il existe des traductions de ses deux articles : "Inversion of Control Containers and the Dependency Injection pattern" et "Inversion of Control" .