Apprivoiser les classes des «fonctions utilitaires»

15

Dans notre base de code Java, je continue de voir le modèle suivant:

/**
 This is a stateless utility class
 that groups useful foo-related operations, often with side effects.
*/
public class FooUtil {
  public int foo(...) {...}
  public void bar(...) {...}
}

/**
 This class does applied foo-related things.
*/
class FooSomething {
  int DoBusinessWithFoo(FooUtil fooUtil, ...) {
    if (fooUtil.foo(...)) fooUtil.bar(...);
  }
}

Ce qui me dérange, c'est que je dois passer une instance de FooUtilpartout, car les tests.

  • Je ne peux pas rendre FooUtilles méthodes de statique, car je ne pourrai pas les simuler pour tester les deux FooUtilet ses classes clientes.
  • Je ne peux pas créer une instance de FooUtilsur le lieu de consommation avec un new, encore une fois parce que je ne pourrai pas m'en moquer pour les tests.

Je suppose que mon meilleur pari est d'utiliser l'injection (et je le fais), mais il ajoute son propre ensemble de tracas. En outre, la transmission de plusieurs instances util gonfle la taille des listes de paramètres de méthode.

Y a-t-il un moyen de mieux gérer cela que je ne vois pas?

Mise à jour: puisque les classes utilitaires sont sans état, je pourrais probablement ajouter un INSTANCEmembre singleton statique ou une getInstance()méthode statique , tout en conservant la possibilité de mettre à jour le champ statique sous-jacent de la classe pour le test. Ne semble pas super propre non plus.

9000
la source
4
Et votre supposition selon laquelle toutes les dépendances doivent être moquées pour faire des tests unitaires efficacement n'est pas correcte (bien que je cherche toujours la question qui en discute).
Telastyn
4
Pourquoi ces méthodes ne sont-elles pas membres de la classe dont elles manipulent les données?
Jules
3
Avoir un ensemble séparé de Junit qui appelle les fns statiques si FooUtility. Ensuite, vous pouvez l'utiliser sans vous moquer. Si vous devez vous moquer de quelque chose, je soupçonne que ce n'est pas seulement un utilitaire mais qu'il fait une certaine logique d'E / S et métier et devrait en fait être une référence de membre de classe et initialisé via le constructeur, la méthode init ou setter.
tgkprog
2
+1 pour le commentaire de Jules. Les classes util sont une odeur de code. Selon la sémantique, il devrait y avoir une meilleure solution. Peut-être que les FooUtilméthodes devraient être introduites FooSomething, peut-être qu'il y a des propriétés FooSomethingqui devraient être modifiées / étendues afin que les FooUtilméthodes ne soient plus nécessaires. Veuillez nous donner un exemple plus concret de ce qui FooSomethingpeut nous aider à fournir une bonne réponse à ces questions.
valenterry
13
@valenterry: La seule raison pour laquelle les classes util sont une odeur de code est que Java ne fournit pas un bon moyen d'avoir des fonctions autonomes. Il y a de très bonnes raisons d'avoir des fonctions qui ne sont pas spécifiques aux classes.
Robert Harvey

Réponses:

16

Tout d'abord, permettez-moi de dire qu'il existe différentes approches de programmation pour différents problèmes. Pour mon travail, je préfère une conception OO solide pour l'orginazation et la maintenance et ma réponse reflète cela. Une approche fonctionnelle aurait une réponse très différente.

J'ai tendance à constater que la plupart des classes d'utilitaires sont créées parce que l'objet sur lequel les méthodes doivent se trouver n'existe pas. Cela vient presque toujours de l'un des deux cas, soit quelqu'un passe des collections, soit quelqu'un passe des haricots (ceux-ci sont souvent appelés POJO mais je pense qu'un tas de setters et de getters est mieux décrit comme un bean).

Lorsque vous avez une collection que vous faites circuler, il doit y avoir du code qui veut faire partie de cette collection, et cela finit par être dispersé dans tout votre système et finalement collecté dans des classes utilitaires.

Ma suggestion est d'envelopper vos collections (ou beans) avec toute autre donnée étroitement associée dans de nouvelles classes et de mettre le code "utilitaire" dans des objets réels.

Vous pouvez étendre la collection et y ajouter les méthodes mais vous perdez ainsi le contrôle de l'état de votre objet - je vous suggère de l'encapsuler complètement et d'exposer uniquement les méthodes de logique métier qui sont nécessaires (pensez "Ne demandez pas à vos objets leurs données, donnez des commandes à vos objets et laissez-les agir sur leurs données privées ", un locataire OO important et qui, quand vous y réfléchissez, nécessite l'approche que je suggère ici).

Cet «encapsulage» est utile pour tous les objets de bibliothèque à usage général qui contiennent des données, mais vous ne pouvez pas ajouter de code à - Mettre du code avec les données qu'il manipule est un autre concept majeur dans la conception OO.

Il existe de nombreux styles de codage où ce concept d'encapsulation serait une mauvaise idée - qui veut écrire une classe entière lors de l'implémentation d'un script ou d'un test unique? Mais je pense que l'encapsulation de tous les types / collections de base que vous ne pouvez pas ajouter de code à vous-même est obligatoire pour OO.

Bill K
la source
Cette réponse est tellement impressionnante. J'ai eu ce problème (évidemment), j'ai lu cette réponse avec un certain doute, je suis retourné et j'ai regardé mon code et j'ai demandé "quel objet manquant devrait avoir ces méthodes". Et voilà, solution élégante trouvée et espace objet-modèle comblé.
GreenAsJade
@Deduplicator En 40 ans de programmation, j'ai rarement (jamais?) Vu le cas où j'étais bloqué par trop de niveaux d'indirection (à part le combat occasionnel avec mon IDE pour passer à une implémentation plutôt qu'à une interface) mais je ' Nous avons souvent (très souvent) vu le cas où les gens écrivaient un code horrible plutôt que de créer une classe clairement nécessaire. Bien que je pense que trop d'indirection aurait pu mordre certaines personnes, je ferais une erreur du côté des principes OO appropriés, comme chaque classe faisant bien une chose, un confinement approprié, etc.
Bill K
@BillK Généralement, une bonne idée. Mais sans trappe d'évacuation, l'encapsulation peut vraiment gêner. Et il y a une certaine beauté à libérer des algorithmes que vous pouvez faire correspondre à n'importe quel type que vous voulez, bien que, certes, dans les langages strict-OO, tout sauf OO est assez maladroit.
Déduplicateur
@Deduplicator Lorsque vous écrivez des fonctions «utilitaires» qui doivent être écrites en dehors des exigences métier spécifiques, vous avez raison. Les classes utilitaires remplies de fonctions (telles que les collections) sont tout simplement maladroites dans OO car elles ne sont pas associées aux données. ce sont les "Escape Hatch" que les gens qui ne sont pas à l'aise avec OO (aussi, les vendeurs d'outils doivent les utiliser pour des fonctionnalités très génériques). Ma suggestion ci-dessus était d'envelopper ces hacks dans des classes lorsque vous travaillez sur la logique métier afin de pouvoir réassocier votre code à vos données.
Bill K
1

Vous pouvez utiliser un modèle de fournisseur et injecter votre objet FooSomethingavec un FooUtilProviderobjet (peut être dans un constructeur; une seule fois).

FooUtilProvideraurait seulement une méthode: FooUtils get();. La classe utiliserait alors l' FooUtilsinstance qu'elle fournit.

C'est maintenant une étape loin de l'utilisation d'un framework d'injection de dépendances, lorsque vous n'auriez qu'à câbler le co-conteneur DI deux fois: pour le code de production et pour votre suite de tests. Vous liez simplement l' FooUtilsinterface à l'un RealFooUtilsou à l'autre MockedFooUtils, et le reste se fait automatiquement. Chaque objet qui en dépend FooUtilsProviderobtient la version appropriée de FooUtils.

Konrad Morawski
la source