web-dev-qa-db-fra.com

Quelles classes devraient être câblées automatiquement par Spring (quand utiliser l'injection de dépendances)?

J'utilise Dependency Injection au printemps depuis un certain temps maintenant, et je comprends comment cela fonctionne et quels sont les avantages et les inconvénients de son utilisation. Cependant, lorsque je crée une nouvelle classe, je me demande souvent - Cette classe doit-elle être gérée par Spring IOC Container?

Et je ne veux pas parler des différences entre l'annotation @Autowired, la configuration XML, l'injection de setter, l'injection de constructeur, etc. Ma question est générale.

Disons que nous avons un service avec un convertisseur:

@Service
public class Service {

    @Autowired
    private Repository repository;

    @Autowired
    private Converter converter;

    public List<CarDto> getAllCars() {
        List<Car> cars = repository.findAll();
        return converter.mapToDto(cars);
    }
}

@Component
public class Converter {

    public CarDto mapToDto(List<Car> cars) {
        return new ArrayList<CarDto>(); // do the mapping here
    }
}

De toute évidence, le convertisseur n'a pas de dépendances, il n'est donc pas nécessaire qu'il soit câblé automatiquement. Mais pour moi, cela semble mieux que auto-câblé. Le code est plus propre et facile à tester. Si j'écris ce code sans DI, le service ressemblera à ça:

@Service
public class Service {

    @Autowired
    private Repository repository;

    public List<CarDto> getAllCars() {
        List<Car> cars = repository.findAll();
        Converter converter = new Converter();
        return converter.mapToDto(cars);
    }
}

Maintenant, il est beaucoup plus difficile de le tester. De plus, un nouveau convertisseur sera créé pour chaque opération de conversion, même s'il est toujours dans le même état, ce qui semble être une surcharge.

Il existe des modèles bien connus dans Spring MVC: contrôleurs utilisant des services et services utilisant des référentiels. Ensuite, si le référentiel est câblé automatiquement (ce qui est généralement le cas), le service doit également être câblé automatiquement. Et c'est assez clair. Mais quand utilisons-nous l'annotation @Component? Si vous avez des classes utilitaires statiques (comme les convertisseurs, les mappeurs) - les attribuez-vous automatiquement?

Essayez-vous de rendre toutes les classes câblées automatiquement? Ensuite, toutes les dépendances de classe sont faciles à injecter (encore une fois, faciles à comprendre et faciles à tester). Ou essayez-vous de câbler automatiquement uniquement lorsque cela est absolument nécessaire?

J'ai passé un certain temps à chercher des règles générales sur l'utilisation du câblage automatique, mais je n'ai trouvé aucun conseil spécifique. Habituellement, les gens parlent de "utilisez-vous DI? (Oui/non)" ou "quel type d'injection de dépendance préférez-vous", ce qui ne répond pas à ma question.

Je serais reconnaissant pour tout conseil concernant ce sujet!

34
Kacper86

Soyez d'accord avec le commentaire de @ ericW, et je veux juste ajouter n'oubliez pas que vous pouvez utiliser des initialiseurs pour garder votre code compact:

@Autowired
private Converter converter;

ou

private Converter converter = new Converter();

ou, si la classe n'a vraiment aucun état

private static final Converter CONVERTER = new Converter();

L'un des critères clés pour savoir si Spring doit instancier et injecter un bean est, est-ce que ce bean est si compliqué que vous voulez vous en moquer lors des tests? Si oui, injectez-le. Par exemple, si le convertisseur effectue un aller-retour vers n'importe quel système externe, faites-en plutôt un composant. Ou si la valeur de retour a un grand arbre de décision avec des dizaines de variations possibles en fonction de l'entrée, alors moquez-la. Et ainsi de suite.

Vous avez déjà fait un bon travail de déploiement de cette fonctionnalité et d'encapsulation, donc maintenant il s'agit juste de savoir si elle est suffisamment complexe pour être considérée comme une "unité" distincte pour les tests.

8
Rob

Je ne pense pas que vous devez @Autowired toutes vos classes, cela devrait dépendre de l'utilisation réelle, pour vos scénarios, il vaut mieux utiliser la méthode statique au lieu de @Autowired. Je ne vois aucun avantage à utiliser @Autowired pour ces classes d'utilitaires simples, et cela augmentera absolument le coût du conteneur Spring s'il n'est pas utilisé correctement.

5
ericW

Ma règle d'or est basée sur quelque chose que vous avez déjà dit: la testabilité. Demandez-vous "Puis-je le tester à l'unité facilement?". Si la réponse est oui, en l'absence de toute autre raison, je serais d'accord avec ça. Donc, si vous développez le test unitaire en même temps que vous le développez, vous économiserez beaucoup de douleur.

Le seul problème potentiel est que si le convertisseur échoue, votre test de service échouera également. Certaines personnes diraient que vous devriez vous moquer des résultats du convertisseur dans les tests unitaires. De cette façon, vous seriez en mesure d'identifier les erreurs plus rapidement. Mais il a un prix: il faut se moquer de tous les résultats des convertisseurs alors que le vrai convertisseur aurait pu faire le travail.

Je suppose également qu'il n'y a aucune raison d'utiliser différents convertisseurs dto.

2
Borjab

TL; DR: Une approche hybride de câblage automatique pour DI et de transfert de constructeur pour DI peut simplifier le code que vous avez présenté.

J'ai regardé des questions similaires en raison de quelques weblogic avec des erreurs/complications de démarrage du framework Spring impliquant des dépendances d'initialisation @autowired bean. J'ai commencé à mélanger dans une autre approche DI: le transfert constructeur. Il requiert des conditions comme celles que vous présentez ("De toute évidence, le convertisseur n'a pas de dépendances, il n'est donc pas nécessaire qu'il soit câblé automatiquement."). Néanmoins, j'aime beaucoup pour la flexibilité et c'est toujours assez simple.

@Service
public class Service {

    @Autowired
    private Repository repository;

    public List<CarDto> getAllCars(Converter converter) {
        List<Car> cars = repository.findAll();
        return converter.mapToDto(cars);
    }
    public List<CarDto> getAllCars() {
        Converter converter = new Converter();
        return getAllCars(converter);
    }
}

ou même comme un rif sur la réponse de Rob

@Service
public class Service {

    @Autowired
    private Repository repository;

    private final Converter converter = new Converter(); // static if safe for that

    public List<CarDto> getAllCars(Converter converter) {
        List<Car> cars = repository.findAll();
        return converter.mapToDto(cars);
    }
    public List<CarDto> getAllCars() {
        return getAllCars(converter);
    }
}

Cela peut ne pas nécessiter de changement d'interface publique, mais je le ferais. Toujours le

public List<CarDto> getAllCars(Converter converter) { ... }

pourrait être protégé ou privé pour être limité à des fins de test/extension uniquement.

Le point clé est que la DI est facultative. Une valeur par défaut est fournie, qui peut être remplacée de manière simple. Il a sa faiblesse car le nombre de champs augmente, mais pour 1, peut-être 2 champs, je préférerais cela dans les conditions suivantes:

  • Très petit nombre de champs
  • La valeur par défaut est presque toujours utilisée (DI est une couture de test) ou la valeur par défaut est presque jamais utilisée (espace d'arrêt) parce que j'aime la cohérence, donc cela fait partie de mon arbre de décision, pas une vraie contrainte de conception

Dernier point (un peu OT, mais lié à la façon dont nous décidons quoi/où @autowired): la classe du convertisseur, telle que présentée, est une méthode utilitaire (aucun champ, aucun constructeur, pourrait être statique). Peut-être que la solution aurait une sorte de méthode mapToDto () dans la classe Cars? Autrement dit, poussez l'injection de conversion vers la définition Cars, où elle est probablement déjà intimement liée:

@Service
public class Service {

   @Autowired
   private Repository repository;

   public List<CarDto> getAllCars() {
    return repository.findAll().stream.map(c -> c.mapToDto()).collect(Collectors.toList()));
   }
}
0
Kristian H