kubuszok.com

1 Fév 2018 • série: Aléatoire à propos de Scala • tags: injection de dépendance de modèle de gâteau scala

Il y a longtemps, au pays de Scala, un nouveau type d’injection de dépendance est apparu. À long terme, cela apporte plus de problèmes qu’il n’en vaut la peine.

Motif de gâteau

Rappelons ce qu’est un motif de gâteau. Pour le comprendre, nous devons connaître les auto-types. Ils étaient destinés à être utilisés comme mixins et ressemblent plus ou moins à ça:

trait UserRepository { def fetchUsers(userIds: Seq): Seq}class UserRepositoryDbImpl(database: Database) extends UserRepository { def fetchUsers(userIds: Seq): Seq = ...}trait UserRepositoryLogging { self: UserRepository => private lazy val logger: Logger = ... override def fetchUsers(userIds: Seq): Seq = { val users = super.fetchUsers(usersIds) logger.debug(s"""Fetched users by ids: ${users.mkString(", ")}""") users }}val userRepository: UserRepository = new UserRepositoryDbImpl(database) with UserRepositoryLogging

Ici, nous pouvons ajouter le trait UserRepositoryLogging à tout ce qui est UserRepository implémentation – il ne compilerait pas autrement. De plus, à l’intérieur de UserRepositoryLogging, nous supposons qu’il s’agit de l’implémentation de UserRepository. Ainsi, tout ce qui est accessible dans UserRepository y est également accessible.

Maintenant, puisque nous pouvons accéder à tout type déclaré utilisé pour l’auto-type, nous sommes autorisés à le faire:

trait UserRepository { def fetchUsers(userIds: Seq): Seq}trait UserRepositoryComponent { def userRepository: UserRepository}trait UserRepositoryComponentImpl extends UserRepositoryComponent { lazy val userRepository: UserRepository = new UserRepository { // ... }}trait SomeController { self: UserRepositoryComponent => def handleRequest(userIds: Seq): Response = { // ... userRepository.fetchUsers(userIds) // ... }}object SomeController extends SomeController with UserRepositoryComponentImpl

Dans SomeController nous déclarons un auto-type, ce qui garantit que son implémentation instanciable aura une méthode userRepository. Donc, en ajoutant le mixin, nous fournissons l’implémentation et nous nous assurons donc que la dépendance est injectée au moment de la compilation. Sans réflexion d’exécution, avec sécurité de type, pas de configurations ou de bibliothèques supplémentaires.

Chacun de ces composants pourrait être une couche de notre application (couche de logique métier, couche d’infrastructure, etc.), que quelqu’un a comparée aux couches du gâteau. Ainsi, en créant votre application de cette manière, vous préparez efficacement un gâteau. Ainsi modèle de gâteau.

Maintenant why pourquoi est-ce un problème?

Ajout de dépendances

Dans les projets plus importants, le nombre de services, de référentiels et d’utilitaires sera sûrement supérieur à 1-2. Ainsi, votre gâteau commencera à ressembler à:

object PaymentTransactionApiController extends TransactionApiController with ConfigComponentImpl with DatabaseComponentImpl with UserRepositoryComponentImpl with SessionRepositoryComponentImpl with SecurityServicesComponentImpl with ExternalPaymentApiServicesComponentImpl with PaymentServicesComponentImpl with TransactionServicesComponentImpl

Du fait que j’ai vu une fois un gâteau où ComponentImpl a traversé 2-3 écrans. Vous n’y injectiez pas seulement les composants nécessaires à la fois, vous combiniez tout ensemble à la fois: infrastructure, services de domaine, persistance, logique métier

Maintenant, imaginez ce qui se passe lorsque vous devez ajouter une dépendance. Vous commencez par ajouter un autre with Dependency à votre auto-type. Ensuite, vous vérifiez où le trait mis à jour est utilisé. Vous devrez peut-être également ajouter un auto-type ici. Vous grimpez dans l’arbre des mixins jusqu’à ce que vous atteigniez une classe ou un objet. Ouf.

Sauf que cette fois, vous l’avez ajouté vous-même, vous savez donc ce qui doit être ajouté. Mais lorsque vous effectuez un rebase ou résolvez un conflit, vous pourriez vous retrouver dans une situation où une nouvelle dépendance est apparue que vous n’avez pas. L’erreur du compilateur indique seulement que self-type X does not conform to Y. Avec plus de 50 composants, autant deviner lequel échoue. (Ou commencez à faire une recherche binaire en supprimant les composants jusqu’à ce que l’erreur disparaisse).

Lorsque la taille du gâteau et la quantité augmentent, on peut vouloir mettre en œuvre la règle SÈCHE et la diviser en petits gâteaux, qui seront assemblés plus tard. Cela révèle de nouvelles profondeurs de l’enfer.

Suppression des dépendances

Avec une DI normale lorsque vous arrêtez d’utiliser un objet, votrecomp / compilateur peut vous dire qu’il n’est plus nécessaire, vous pouvez donc le supprimer. Avec le modèle de gâteau, vous ne serez pas informé de telles choses.

En conséquence, le nettoyage est beaucoup plus difficile et il est fort possible qu’à un moment donné, vous vous retrouviez avec beaucoup plus de dépendances que ce dont vous avez vraiment besoin.

Temps de compilation

Tout cela s’ajoute au temps de compilation. Ce n’est peut-être pas évident à première vue, mais vous pouvez vous retrouver avec:

trait ModuleAComponentImpl { self: ModuleBComponent => }trait ModuleBComponentImpl { self: ModuleAComponent => }

qui est une dépendance cyclique. (Nous savons tous qu’ils sont mauvais, mais vous ne remarquerez peut-être même pas que vous venez d’en créer un. Surtout si vous initialisez tout paresseusement).

Donc, ces 2 traits seront très souvent compilés ensemble, et le compilateur incrémental Zinc n’aidera pas à cela. De plus, les modifications apportées à l’un des composants d’un gâteau déclencheront la recompilation des éléments ci-dessus dans le graphique de dépendance.

Tester

Les dépendances cycliques créent un autre problème. Vous pourriez très bien vous retrouver avec quelque chose comme:

trait AComponentImpl { self: BComponent => lazy val a: A = new A { b.someMethod() // use b }}trait BComponentImpl { self: AComponent => lazy val b: B = new B { a.someMethod() // use a }}object Cake extends AComponentImpl with BComponentImplCake.a // NPE!Cake.b // NPE!

Chaque composant sera dans son propre fichier, vous ne remarquerez donc pas cette dépendance. Les tests avec mock ne vous aideront pas avec cela:

trait BComponentMock { val b: B = mock }val aComponent = new AComponentImpl with BComponentMock {}aComponent.a // works!

donc tout ira bien jusqu’à ce que vous exécutiez réellement le code. Vous savez – le code est sûr du type, ce qui signifie que la valeur que vous allez construire suivra les contraintes de type. Mais ces contraintes ne signifient rien car Nothing (exception levée) est un sous-type parfaitement valide de votre type requis.

Ordre d’initialisation

Le problème précédent est lié à celui-ci : quel est l’ordre d’initialisation du composant ?

Avec la DI normale, c’est évident – avant de mettre une chose dans l’autre, vous devez la créer. Donc, vous regardez simplement l’ordre dans lequel vous créez des objets.

Avec un gâteau, vous pouvez l’oublier. Les valeurs dans les composants seront souvent lazy vals ou objects. Ainsi, l’attribut le premier accédé essaiera d’instancier ses dépendances, qui essaiera d’instancier ses dépendances, etc.

Même si nous allons avec vals c’est une question d’ordre dans lequel nous avons composé du gâteau – vous savez, la linéarisation des traits. Tant que chaque composant sera utilisé exactement une fois – pas de problème. Mais si vous avez séché votre gâteau, et que certains composants se répètent, lorsque vous les fusionnez

Dans un monde parfait, des choses telles que l’ordre d’initialisation ne devraient pas avoir d’importance, mais assez souvent, c’est le cas, et nous aimerions savoir par exemple quand une connexion à la base de données ou un abonnement au sujet Kafka a commencé, et consigner les choses afin de déboguer les échecs potentiels.

Passe-passe et verrouillage

Comme vous l’avez probablement remarqué, ce modèle génère beaucoup de passe-passe – fondamentalement, chaque référentiel ou service devrait avoir un wrapper, uniquement pour annoter les dépendances.

Une fois que vous descendez cette route, vous remarquerez qu’il est assez difficile de se débarrasser du motif de gâteau. Chaque implémentation est placée dans un trait, et ses dépendances proviennent de la portée, pas via des paramètres. Donc, pour chaque classe, vous devrez d’abord effectuer un refactorisation afin de pouvoir l’instancier sans le wrapper.

Mais alors les dépendances devraient également être refactorisées et ainsi de suite. Le modèle de gâteau est un engagement laid, qui vous fait mal et vous oppose lorsque vous essayez de vous en retirer progressivement.

Littéralement, toute autre forme de DI en Scala, que je connais (réflexion d’exécution a’la Guice, annotations de macro a’la MacWire, implicits, combinaisons de ces 3) vous permet de dire stop! à tout moment, et passer progressivement à une autre solution.

Résumé

Le motif de gâteau est une solution à un problème de DI qui, à long terme, fait plus de mal que de bien. D’après ce que j’ai entendu, il a été utilisé dans le compilateur Scala, et ils le regrettent. C’était le mécanisme de DI par défaut pour le framework Play, mais à cause de tous ces problèmes, les auteurs ont décidé de passer à Guice – oui, les programmeurs fonctionnels préféraient la réflexion d’exécution au gâteau sécurisé, ce qui en dit long.

Alors, quelles sont les alternatives?

  • le simple passage d’arguments manuels – pas besoin d’expliquer que
  • les implicits – ont ses avantages jusqu’à ce que votre projet soit maintenu par plus que vous-même, et que vos collègues commencent à se plaindre de ne pas savoir, de ce qui se passe, de la compilation lente, de ne pas comprendre les implicits
  • runtime reflection-Guice est un framework mature qui fait DI exactement de la manière que je déteste. Il a été utilisé dans de nombreux projets, les programmeurs Java l’adorent – tout comme ils aiment le printemps. Certains d’entre eux laissent fuir l’abstraction en passant l’injecteur dans la classe. Certains d’entre eux veulent que leur configuration soit flexible – si flexible que le câblage de classe pourrait échouer à l’exécution, bleh!
  • MacWire – à mi-chemin de la transmission manuelle des arguments et de l’utilisation de macros pour tout – il analyse la portée actuelle au moment de la compilation et injecte des dépendances pour vous. Si certaines sont manquantes, la compilation échoue. Vous écrivez entre-temps juste wire
  • Cadre basé sur des macro-cellules où vous construisez une recette pour DI avec un DSL. Il prend en charge la gestion du cycle de vie des objets
  • pulp – mon auto-promotion éhontée. Il a utilisé des implicits et des annotations de macro afin de générer des fournisseurs pour les classes

À mon humble avis, chacun d’eux devrait fonctionner mieux que le modèle de gâteau.

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée.