Usine statique vs usine en tant que singleton

24

Dans certains de mes codes, j'ai une usine statique similaire à celle-ci:

public class SomeFactory {

    // Static class
    private SomeFactory() {...}

    public static Foo createFoo() {...}

    public static Foo createFooerFoo() {...}
}

Lors d'une révision du code, il a été proposé que ce soit un singleton et injecté. Donc, cela devrait ressembler à ceci:

public class SomeFactory {

    public SomeFactory() {}

    public Foo createFoo() {...}

    public Foo createFooerFoo() {...}
}

Quelques points à souligner:

  • Les deux usines sont apatrides.
  • La seule différence entre les méthodes est leur portée (instance vs statique). Les implémentations sont les mêmes.
  • Foo est un bean qui n'a pas d'interface.

Les arguments que j'avais pour devenir statique étaient:

  • La classe est sans état, elle n'a donc pas besoin d'être instanciée
  • Il semble plus naturel de pouvoir appeler une méthode statique que d'avoir à instancier une fabrique

Les arguments pour l'usine en tant que singleton étaient les suivants:

  • C'est bon de tout injecter
  • Malgré l'apatridie de l'usine, le test est plus facile avec l'injection (facile à se moquer)
  • Il faut se moquer lors du test du consommateur

J'ai de sérieux problèmes avec l'approche singleton car elle semble suggérer qu'aucune méthode ne devrait jamais être statique. Cela semble également suggérer que des utilitaires tels que ceux-ci StringUtilsdevraient être enveloppés et injectés, ce qui semble idiot. Enfin, cela implique que je devrai se moquer de l'usine à un moment donné, ce qui ne semble pas correct. Je ne peux pas penser au moment où je devrais me moquer de l'usine.

Que pense la communauté? Bien que je n'aime pas l'approche singleton, je ne semble pas avoir d'argument terriblement fort contre.

bstempi
la source
2
L'usine est utilisée par quelque chose . Pour tester cette chose isolément, vous devez lui fournir une maquette de votre usine. Si vous ne vous moquez pas de votre usine, un bogue peut provoquer un test unitaire échoué alors qu'il ne devrait pas.
Mike
Alors, préconisez-vous contre les utilitaires statiques en général, ou tout simplement les usines statiques?
bstempi
1
Je plaide contre eux en général. En C # par exemple, les classes DateTimeet Filesont notoirement difficiles à tester pour exactement les mêmes raisons. Par exemple, si vous avez une classe qui définit la Createddate DateTime.Nowdans le constructeur, comment allez-vous créer un test unitaire avec deux de ces objets qui ont été créés à 5 minutes d'intervalle? Et des années à part? Vous ne pouvez vraiment pas le faire (sans beaucoup de travail).
Mike
2
Votre usine singleton ne devrait-elle pas avoir un privateconstructeur et une getInstance()méthode? Désolé, incorrigible nit-picker!
TMN
1
@Mike - D'accord - J'ai souvent utilisé une classe DateTimeProvider qui renvoie trivialement DateTime.Now. Vous pouvez ensuite l'échanger contre une maquette qui renvoie une heure prédéterminée, dans vos tests. C'est un travail supplémentaire de le transmettre à tous les constructeurs - le plus gros casse-tête est lorsque les collègues n'y adhèrent pas à 100% et glissent des références explicites à DateTime.Maintenant par paresse ... :)
Julia Hayward

Réponses:

15

Pourquoi sépareriez-vous votre usine du type d'objet qu'elle crée?

public class Foo {

    // Private constructor
    private Foo(...) {...}

    public static Foo of(...) { return new Foo(...); }
}

C'est ce que Joshua Bloch décrit comme son article 1 à la page 5 de son livre, "Java efficace". Java est assez verbeux sans ajouter de classes et singletons supplémentaires et ainsi de suite. Il y a des cas où une classe d'usine séparée a du sens, mais ils sont rares.

Pour paraphraser l'élément 1 de Joshua Bloch, contrairement aux constructeurs, les méthodes d'usine statique peuvent:

  • Avoir des noms qui peuvent décrire des types spécifiques de création d'objets (Bloch utilise probablePrime (int, int, Random) comme exemple)
  • Renvoyer un objet existant (pensez: poids mouche)
  • Renvoyer un sous-type de la classe d'origine ou une interface qu'il implémente
  • Réduire la verbosité (en spécifiant les paramètres de type - voir Bloch par exemple)

Désavantages:

  • Les classes sans constructeur public ou protégé ne peuvent pas être sous-classées (mais vous pouvez utiliser des constructeurs protégés et / ou le point 16: privilégier la composition à l'héritage)
  • Les méthodes de fabrique statique ne se distinguent pas des autres méthodes statiques (utilisez un nom standard comme () ou valueOf ())

Vraiment, vous devriez apporter le livre de Bloch à votre réviseur de code et voir si vous pouvez tous les deux accepter le style de Bloch.

GlenPeterson
la source
J'y ai pensé aussi, et je pense que j'aime ça. Je ne me souviens pas pourquoi je les ai séparés en premier lieu.
bstempi
D'une manière générale, nous sommes d'accord sur le style de Bloch. En fin de compte, nous allons déplacer nos méthodes d'usine dans la classe qu'elles instancient. La seule différence entre notre solution et la vôtre est que le constructeur restera public afin que les gens puissent toujours instancier directement la classe. Merci pour votre réponse!
bstempi
Mais ... c'est horrible. Vous voulez très probablement une classe qui implémente IFoo et la faire passer. En utilisant ce modèle, ce n'est plus possible; vous devez coder en dur IFoo pour signifier Foo afin d'obtenir des instances. Si IFooFactory contient un IFoo create (), le code client n'a pas besoin de connaître le détail d'implémentation de quel Foo concret a été choisi comme IFoo.
Wilbert
@Wilbert public static IFoo of(...) { return new Foo(...); } Quel est le problème? Bloch répertorie la satisfaction de vos préoccupations comme l'un des avantages de cette conception (deuxième puce ci-dessus). Je ne dois pas comprendre votre commentaire.
GlenPeterson
Afin de créer une instance, cette conception attend: Foo.of (...). Cependant, l'existence de Foo ne doit jamais être révélée, seul IFoo doit être connu (car Foo n'est qu'un implément concret d'IFoo). Avoir une méthode d'usine statique sur l'implément concret casse cela et conduit à un couplage entre le code client et l'implémentation concrète.
Wilbert
5

Juste au-dessus de ma tête, voici quelques-uns des problèmes avec la statique en Java:

  • les méthodes statiques ne jouent pas selon les "règles" de POO. Vous ne pouvez pas forcer une classe à implémenter des méthodes statiques spécifiques (pour autant que je sache). Vous ne pouvez donc pas avoir plusieurs classes implémentant le même ensemble de méthodes statiques avec les mêmes signatures. Donc , vous êtes coincé avec un de quoi que ce soit que vous faites. Vous ne pouvez pas non plus vraiment sous-classer une classe statique. Donc pas de polymorphisme, etc.

  • puisque les méthodes ne sont pas des objets, les méthodes statiques ne peuvent pas être transmises et elles ne peuvent pas être utilisées (sans faire une tonne de codage standard), sauf par du code qui leur est lié de manière rigide - ce qui rend le code client hautement accouplé. (Pour être juste, ce n'est pas uniquement la faute de la statique).

  • les champs statiques sont assez similaires aux globaux


Vous avez mentionné:

J'ai de sérieux problèmes avec l'approche singleton car elle semble suggérer qu'aucune méthode ne devrait jamais être statique. Il semble également suggérer que des utilitaires tels que StringUtils devraient être enveloppés et injectés, ce qui semble idiot. Enfin, cela implique que je devrai se moquer de l'usine à un moment donné, ce qui ne semble pas correct. Je ne peux pas penser au moment où je devrais me moquer de l'usine.

Ce qui me rend curieux de vous demander:

  • quels sont, selon vous, les avantages des méthodes statiques?
  • croyez-vous que StringUtilsc'est correctement conçu et mis en œuvre?
  • pourquoi pensez-vous que vous n'aurez jamais besoin de vous moquer de l'usine?

la source
Hé, merci pour la réponse! Dans l'ordre que vous avez demandé: (1) Les avantages des méthodes statiques sont le sucre syntaxique et l'absence d'une instance inutile. C'est bien de lire du code qui a une importation statique et de dire quelque chose comme Foo f = createFoo();plutôt que d'avoir une instance nommée. L'instance elle-même ne fournit aucun utilitaire. (2) Je pense que oui. Il a été conçu pour surmonter le fait que beaucoup de ces méthodes n'existent pas dans la Stringclasse. Remplacer quelque chose d'aussi fondamental que Stringn'est pas une option, donc les utils sont la voie à suivre. (suite)
bstempi
Ils sont faciles à utiliser et ne vous obligent pas à vous soucier des instances. Ils se sentent naturels. (3) Parce que mon usine est très similaire aux méthodes d'usine Integer. Ils sont très simples, ont très peu de logique et ne sont là que pour vous faciliter la tâche (par exemple, il n'y a pas d'autre raison OO de les avoir). La partie principale de mon argument est qu'ils ne dépendent d'aucun état - il n'y a aucune raison de les isoler pendant les tests. Les gens ne se moquent pas StringUtilslors des tests - pourquoi auraient-ils besoin de se moquer de cette simple usine de commodité?
bstempi
3
Ils ne se moquent pas StringUtilscar il y a une assurance raisonnable que les méthodes là-bas n'ont pas d'effets secondaires.
Robert Harvey
@RobertHarvey Je suppose que je fais la même affirmation à propos de mon usine - cela ne change rien. Tout comme StringUtilsrenvoie un résultat sans modifier les entrées, mon usine ne modifie également rien.
bstempi
@bstempi J'y allais pour un peu de méthode socratique. Je suppose que je le suce.
0

Je pense que l'application que vous créez doit être assez simple, sinon vous êtes relativement tôt dans son cycle de vie. Le seul autre scénario auquel je peux penser où vous n'auriez pas déjà rencontré de problèmes avec votre statique est si vous avez réussi à limiter les autres parties de votre code qui connaissent votre Factory à un ou deux endroits.

Imaginez que votre application évolue et maintenant, dans certains cas, vous devez produire une FooBar(sous-classe de Foo). Maintenant, vous devez parcourir tous les endroits de votre code qui connaissent votre SomeFactoryet écrire une sorte de logique qui regarde someConditionet appelle SomeFactoryou SomeOtherFactory. En fait, en regardant votre exemple de code, c'est pire que ça. Vous prévoyez d'ajouter simplement méthode après méthode pour créer différents Foos et tout votre code client doit vérifier une condition avant de déterminer quel Foo faire. Ce qui signifie que tous les clients doivent avoir une connaissance intime du fonctionnement de votre usine et qu'ils doivent tous changer chaque fois qu'un nouveau Foo est nécessaire (ou supprimé!).

Imaginez maintenant si vous venez de créer un IFooFactoryqui fait un IFoo. Maintenant, produire une sous-classe différente ou même une implémentation complètement différente est un jeu d'enfant - vous alimentez simplement la bonne IFooFactory plus haut dans la chaîne, puis lorsque le client l'obtient, il appelle gimmeAFoo()et obtiendra le bon foo parce qu'il a la bonne usine .

Vous vous demandez peut-être comment cela se joue dans le code de production. Pour vous donner un exemple de la base de code sur laquelle je travaille qui commence à se stabiliser après un peu plus d'un an de lancement de projets, j'ai un tas d'usines qui sont utilisées pour créer des objets de données Question (ce sont des modèles de présentation ). J'ai un QuestionFactoryMapqui est fondamentalement un hachage pour rechercher quelle usine de questions utiliser quand. Donc, pour créer une application qui pose différentes questions, j'ai mis différentes usines sur la carte.

Les questions peuvent être présentées dans des instructions ou des exercices (que les deux implémentent IContentBlock), de sorte que chacun InstructionsFactoryou ExerciseFactoryobtiendra une copie de QuestionFactoryMap. Ensuite, les différentes usines d'instructions et d'exercices sont remises à celui ContentBlockBuilderqui conserve à nouveau un hachage à utiliser quand. Et puis, lorsque le XML est chargé, ContentBlockBuilder appelle la fabrique d'exercices ou d'instructions hors du hachage et le lui demande createContent(), puis la fabrique délègue la construction des questions à tout ce qu'elle retire de sa QuestionFactoryMap interne en fonction de ce que est dans chaque nœud XML par rapport au hachage. Malheureusement , cette chose est au BaseQuestionlieu d'unIQuestion, mais cela montre que même lorsque vous faites attention à la conception, vous pouvez faire des erreurs qui peuvent vous hanter sur toute la ligne.

Votre instinct vous dit que ces statiques vous blesseront, et il y a certainement beaucoup de choses dans la littérature pour soutenir votre intestin. Vous ne perdrez jamais en ayant une injection de dépendances que vous n'utiliserez que pour vos tests unitaires (pas que je pense que si vous construisez un bon code que vous ne vous retrouverez pas à l'utiliser plus que cela), mais la statique peut complètement détruire votre capacité pour modifier rapidement et facilement votre code. Alors pourquoi même y aller?

Amy Blankenship
la source
Pensez un instant aux méthodes d'usine de la Integerclasse - des choses simples comme la conversion à partir d'une chaîne. C'est Fooça. Mon Foon'est pas censé être étendu (ma faute de ne pas l'avoir dit), donc je n'ai pas vos problèmes polymorphes. Je comprends la valeur d'avoir des usines polymorphes et les ai utilisées dans le passé ... Je ne pense tout simplement pas qu'elles soient appropriées ici. Pourriez-vous imaginer si vous devez instancier une usine pour effectuer les opérations simples Integerqui sont actuellement intégrées via des usines statiques?
bstempi
De plus, vos hypothèses sur l'application sont assez fausses. L'application est assez impliquée. Ceci Foo, cependant, est beaucoup plus simple que la plupart des classes. C'est juste un simple POJO.
bstempi
Si Foo est destiné à être étendu, alors il y a une raison pour que Factory soit une instance - vous fournissez l'instance correcte de l'usine pour vous donner la bonne sous-classe plutôt que de if (this) then {makeThisFoo()} else {makeThatFoo()}tout appeler via votre code. Une chose que j'ai remarquée chez les personnes dont l'architecture est chroniquement mauvaise, c'est qu'elles sont comme des personnes souffrant de douleur chronique. Ils ne savent pas vraiment ce que ça fait de ne pas souffrir, donc la douleur dans laquelle ils se trouvent ne semble pas si grave.
Amy Blankenship
En outre, vous voudrez peut-être consulter ce lien sur les problèmes avec les méthodes statiques (même les mathématiques!)
Amy Blankenship
J'ai mis à jour la question. Vous et une autre personne avez mentionné l'extension Foo. À juste titre donc - je ne l'ai jamais déclaré définitif. Fooest un bean qui n'est pas destiné à être étendu.
bstempi