Équilibrer l'injection de dépendance avec la conception d'API publique


13

J'ai réfléchi à la manière d'équilibrer la conception testable à l'aide de l'injection de dépendances avec la fourniture d'une API publique fixe simple. Mon dilemme est: les gens voudraient faire quelque chose comme ça var server = new Server(){ ... }et ne devraient pas avoir à se soucier de créer les nombreuses dépendances et le graphique des dépendances que Server(,,,,,,)peut avoir un. Pendant le développement, je ne m'inquiète pas trop, car j'utilise un framework IoC / DI pour gérer tout cela (je n'utilise pas les aspects de gestion du cycle de vie d'un conteneur, ce qui compliquerait les choses).

Désormais, il est peu probable que les dépendances soient réimplémentées. La composanteisation dans ce cas est presque purement pour la testabilité (et une conception décente!) Plutôt que de créer des coutures pour l'extension, etc. Les gens voudront 99,999% du temps souhaiter utiliser une configuration par défaut. Donc. Je pourrais coder en dur les dépendances. Je ne veux pas faire ça, nous perdons nos tests! Je pourrais fournir un constructeur par défaut avec des dépendances codées en dur et un qui accepte les dépendances. C'est ... désordonné et susceptible de prêter à confusion, mais viable. Je pourrais rendre le constructeur de réception de dépendance interne et faire de mes tests unitaires un assemblage ami (en supposant C #), qui nettoie l'API publique mais laisse un piège caché méchant qui se cache pour la maintenance. Avoir deux constructeurs qui sont implicitement connectés plutôt qu'explicitement serait une mauvaise conception en général dans mon livre.

En ce moment, c'est à peu près le moins de mal auquel je puisse penser. Des avis? Sagesse?

Réponses:


11

L'injection de dépendance est un modèle puissant lorsqu'elle est bien utilisée, mais trop souvent ses praticiens deviennent dépendants d'un cadre externe. L'API résultante est assez pénible pour ceux d'entre nous qui ne veulent pas lier librement notre application avec du ruban adhésif XML. N'oubliez pas Plain Old Objects (POO).

Réfléchissez d'abord à la façon dont vous vous attendriez à ce que quelqu'un utilise votre API - sans le cadre impliqué.

  • Comment instanciez-vous le serveur?
  • Comment étendez-vous le serveur?
  • Que voulez-vous exposer dans l'API externe?
  • Que voulez-vous cacher dans l'API externe?

J'aime l'idée de fournir des implémentations par défaut pour vos composants et le fait que vous ayez ces composants. L'approche suivante est une pratique d'OO parfaitement valide et décente:

public Server() 
    : this(new HttpListener(80), new HttpResponder())
{}

public Server(Listener listener, Responder responder)
{
    // ...
}

Tant que vous documentez les implémentations par défaut des composants dans les documents de l'API et que vous fournissez un constructeur qui effectue toute la configuration, tout devrait bien se passer. Les constructeurs en cascade ne sont pas fragiles tant que le code de configuration se produit dans un constructeur maître.

Essentiellement, tous les constructeurs faisant face au public auraient les différentes combinaisons de composants que vous souhaitez exposer. En interne, ils remplissent les valeurs par défaut et s'en remettent à un constructeur privé qui termine la configuration. En substance, cela vous donne un peu de SEC et est assez facile à tester.

Si vous devez fournir un friendaccès à vos tests pour configurer l'objet Serveur afin de l'isoler pour les tests, allez-y. Il ne fera pas partie de l'API publique, ce à quoi vous devez faire attention.

Soyez juste gentil avec vos consommateurs d'API et ne nécessite pas de framework IoC / DI pour utiliser votre bibliothèque. Pour avoir une idée de la douleur que vous infligez, assurez-vous que vos tests unitaires ne dépendent pas non plus du cadre IoC / DI. (C'est une bête noire personnelle, mais dès que vous introduisez le framework, ce n'est plus un test unitaire - il devient un test d'intégration).


Je suis d'accord, et je ne suis certainement pas fan de la soupe de config IoC - la configuration XML n'est pas quelque chose que j'utilise du tout en ce qui concerne l'IoC. Exiger certainement l'utilisation d'IoC est exactement ce que j'évitais - c'est le genre d'hypothèse qu'un concepteur d'API public ne peut jamais faire. Merci pour le commentaire, c'est dans le sens que je pensais et c'est rassurant d'avoir un bilan de santé de temps en temps!
kolektiv

-1 Cette réponse dit essentiellement "n'utilisez pas l'injection de dépendance" et contient des hypothèses inexactes. Dans votre exemple, si le HttpListenerconstructeur change, la Serverclasse doit également changer. Ils sont couplés. Une meilleure solution est ce que dit @Winston Ewert ci-dessous, qui est de fournir une classe par défaut avec des dépendances pré-spécifiées, en dehors de l'API de la bibliothèque. Ensuite, le programmeur n'est pas obligé de configurer toutes les dépendances puisque vous prenez la décision pour lui, mais il a toujours la possibilité de les modifier plus tard. C'est un gros problème lorsque vous avez des dépendances tierces.
M. Dudley

FWIW l'exemple fourni est appelé "injection bâtarde". stackoverflow.com/q/2045904/111327 stackoverflow.com/q/6733667/111327
M. Dudley

1
@MichaelDudley, avez-vous lu la question du PO? Toutes les "hypothèses" étaient basées sur les informations fournies par le PO. Pendant un certain temps, "l'injection bâtarde" comme vous l'avez appelé, était le format d'injection de dépendance privilégié - en particulier pour les systèmes dont les compositions ne changent pas pendant l'exécution. Les setters et les getters sont également une autre forme d'injection de dépendance. J'ai simplement utilisé des exemples basés sur ce que le PO fournissait.
Berin Loritsch

4

En Java, dans de tels cas, il est habituel d'avoir une configuration "par défaut" construite par une usine, maintenue indépendante du serveur qui se comporte "correctement". Ainsi, votre utilisateur écrit:

var server = DefaultServer.create();

tandis que le Serverconstructeur de accepte toujours toutes ses dépendances et peut être utilisé pour une personnalisation approfondie.


+1, SRP. La responsabilité d'un cabinet dentaire n'est pas de construire un cabinet dentaire. La responsabilité d'un serveur n'est pas de construire un serveur.
R. Schmitz

2

Que diriez-vous de fournir une sous-classe qui fournit les "API publiques"

class StandardServer : Server
{
    public StandardServer():
        this( depends1, depends2, depends3)
    {
    }
}

L'utilisateur peut new StandardServer()et être en route. Ils peuvent également utiliser la classe de base Server s'ils souhaitent plus de contrôle sur le fonctionnement du serveur. C'est plus ou moins l'approche que j'utilise. (Je n'utilise pas de framework car je n'ai pas encore vu le point.)

Cela expose toujours l'api interne, mais je pense que vous devriez. Vous avez divisé vos objets en différents composants utiles qui devraient fonctionner indépendamment. Il est impossible de savoir quand un tiers peut vouloir utiliser l'un des composants de manière isolée.


1

Je pourrais rendre le constructeur de réception de dépendance interne et faire de mes tests unitaires un assemblage ami (en supposant C #), qui nettoie l'API publique mais laisse un piège caché méchant qui se cache pour la maintenance.

Cela ne me semble pas être un "piège méchant caché". Le constructeur public devrait simplement appeler celui interne avec les dépendances "par défaut". Tant qu'il est clair que le constructeur public ne doit pas être modifié, tout devrait bien se passer.


0

Je suis totalement d'accord avec votre opinion. Nous ne voulons pas polluer la limite d'utilisation des composants uniquement à des fins de tests unitaires. C'est tout le problème de la solution de test basée sur DI.

Je suggère d'utiliser le modèle InjectableFactory / InjectableInstance . InjectableInstance est une simple classe d'utilitaires réutilisable qui est titulaire d'une valeur mutable . L'interface du composant contient une référence à ce détenteur de valeur qui s'est initialisé avec l'implémentation par défaut. L'interface fournira une méthode singleton get () qui déléguera au détenteur de la valeur. Le client composant appellera la méthode get () au lieu de new pour obtenir une instance d'implémentation afin d'éviter une dépendance directe. Pendant le temps de test, nous pouvons éventuellement remplacer l'implémentation par défaut par une maquette. Après, je vais utiliser un exemple TimeProviderqui est une abstraction de Date (), ce qui permet aux tests unitaires d'injecter et de simuler pour simuler différents moments. Désolé, je vais utiliser java ici car je suis plus familier avec java. C # devrait être similaire.

public interface TimeProvider {
  // A mutable value holder with default implementation
  InjectableInstance<TimeProvider> instance = InjectableInstance.of(Impl.class);  
  static TimeProvider get() { return instance.get(); }  // Singleton method.

  class Impl implements TimeProvider {        // Default implementation                                    
    @Override public Date getDate() { return new Date(); }
    @Override public long getTimeMillis() { return System.currentTimeMillis(); }
  }

  class Mock implements TimeProvider {   // Mock implemention
    @Setter @Getter long timeMillis = System.currentTimeMillis();
    @Override public Date getDate() { return new Date(timeMillis); }
    public void add(long offset) { timeMillis += offset; }
  }

  Date getDate();
  long getTimeMillis();
}

// The client of TimeProvider
Order order = new Order().setCreationDate(TimeProvider.get().getDate()));

// In the unit testing
TimeProvider.Mock timeMock = new TimeProvider.Mock();
TimeProvider.instance.setInstance(timeMock);  // Inject mock implementation

L'InjectableInstance est assez facile à implémenter, j'ai une implémentation de référence en java. Pour plus de détails, veuillez vous référer à mon article de blog Indépendance de dépendance avec Injectable Factory


Alors ... vous remplacez l'injection de dépendance par un singleton mutable? Cela semble horrible, comment feriez-vous des tests indépendants? Ouvrez-vous un nouveau processus pour chaque test ou les forcez-vous dans une séquence spécifique? Java n'est pas mon point fort, ai-je mal compris quelque chose?
nvoigt

Dans le projet Java maven, l'instance partagée n'est pas un problème car le moteur junit exécutera chaque test dans un chargeur de classe différent par défaut, cependant je rencontre ce problème dans certains IDE car il peut réutiliser le même chargeur de classe. Pour résoudre ce problème, la classe fournit une méthode de réinitialisation à appeler pour réinitialiser à l'état d'origine après chaque test. Dans la pratique, c'est une situation rare où un test différent veut utiliser une maquette différente. Nous appliquons ce modèle pour des projets à grande échelle depuis des années, tout fonctionne correctement, nous avons apprécié un code lisible et propre sans sacrifier la portabilité et la testabilité.
Jianwu Chen
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.