Pourquoi devrais-je utiliser une classe d'usine au lieu d'une construction d'objet directe?

162

J'ai vu l'historique de plusieurs projets de bibliothèques de classes С # et Java sur GitHub et CodePlex, et je constate une tendance à passer aux classes d'usine plutôt qu'à l'instanciation directe d'objet.

Pourquoi devrais-je utiliser les classes d'usine de manière intensive? J'ai une très bonne bibliothèque, où les objets sont créés à l'ancienne - en invoquant des constructeurs publics de classes. Lors du dernier engagement, les auteurs ont rapidement transformé tous les constructeurs publics de milliers de classes en internes et ont également créé une énorme classe factory avec des milliers de CreateXXXméthodes statiques qui ne renvoient que de nouveaux objets en appelant les constructeurs internes des classes. L'API de projet externe est cassée, bien faite.

Pourquoi un tel changement serait-il utile? Quel est le but de la refactorisation de cette façon? Quels sont les avantages de remplacer les appels vers des constructeurs de classe publique par des appels de méthode d'usine statique?

Quand devrais-je utiliser des constructeurs publics et quand devrais-je utiliser des usines?

Rufanov
la source
1
Qu'est-ce qu'une classe / méthode de tissu?
Doval

Réponses:

56

Les classes d'usine sont souvent mises en œuvre car elles permettent au projet de suivre de plus près les principes SOLID . En particulier, les principes de ségrégation d’interface et d’inversion de dépendance.

Les usines et les interfaces permettent une plus grande flexibilité à long terme. Il permet une conception plus découplée - et donc plus testable -. Voici une liste non exhaustive des raisons pour lesquelles vous pourriez vous engager dans cette voie:

  • Il vous permet d’introduire facilement un conteneur Inversion of Control (IoC)
  • Cela rend votre code plus testable car vous pouvez vous moquer d'interfaces
  • Cela vous donne beaucoup plus de flexibilité lorsque vient le temps de changer d'application (vous pouvez créer de nouvelles implémentations sans changer le code dépendant)

Considérez cette situation.

Assemblée A (-> signifie dépend de):

Class A -> Class B
Class A -> Class C
Class B -> Class D

Je souhaite déplacer la classe B vers l’assemblage B, qui dépend de l’Assemblée A. Avec ces dépendances concrètes, je dois déplacer la majeure partie de la hiérarchie de ma classe. Si j'utilise des interfaces, je peux éviter beaucoup de douleur.

Assemblée A:

Class A -> Interface IB
Class A -> Interface IC
Class B -> Interface IB
Class C -> Interface IC
Class B -> Interface ID
Class D -> Interface ID

Je peux maintenant passer de la classe B à l’assemblage B sans aucune douleur. Cela dépend toujours des interfaces de l'assemblage A.

L'utilisation d'un conteneur IoC pour résoudre vos dépendances vous permet encore plus de flexibilité. Il n'est pas nécessaire de mettre à jour chaque appel au constructeur chaque fois que vous modifiez les dépendances de la classe.

Suivre le principe de ségrégation d’interface et le principe d’inversion de dépendance nous permet de construire des applications très flexibles et découplées. Une fois que vous avez travaillé sur l’un de ces types d’applications, vous ne voudrez plus jamais utiliser le newmot - clé.

Stephen
la source
40
Les usines IMO sont la chose la moins importante pour SOLID. Vous pouvez faire SOLID très bien sans usines.
Euphoric
26
Une chose qui n’a jamais eu de sens pour moi, c’est que si vous utilisez des usines pour fabriquer de nouveaux objets, vous devez créer l’usine en premier lieu. Alors qu'est-ce que cela vous apporte exactement? Est-il supposé que quelqu'un d'autre vous donnera l'usine au lieu de l'installer vous-même, ou quelque chose d'autre? Cela devrait être mentionné dans la réponse, sinon on ne sait pas comment les usines résolvent réellement un problème.
Mehrdad
8
@ BЈовић - Sauf que chaque fois que vous ajoutez une nouvelle implémentation, vous devez maintenant ouvrir l'usine et la modifier pour tenir compte de la nouvelle implémentation - violant explicitement OCP.
Telastyn
6
@Telastyn Oui, mais le code utilisant les objets créés ne change pas. C’est plus important que les modifications apportées à l’usine.
Octobre
8
Les usines sont utiles dans certains domaines que la réponse a abordés. Ils ne sont pas appropriés pour utiliser partout . Par exemple, utiliser des usines pour créer des chaînes ou des listes irait trop loin. Même pour les UDT, ils ne sont pas toujours nécessaires. La clé consiste à utiliser une fabrique lorsque l'implémentation exacte d'une interface doit être découplée.
126

Comme ce que dit Whatsisname , je crois que c’est le cas de la conception de logiciels culte cargo . Les usines, en particulier le type abstrait, ne sont utilisables que lorsque votre module crée plusieurs instances d'une classe et que vous souhaitez donner à l'utilisateur de ce module la possibilité de spécifier le type à créer. Cette exigence est en fait assez rare, car la plupart du temps, vous n'avez besoin que d'une instance et vous pouvez simplement transmettre cette instance directement au lieu de créer une fabrique explicite.

Le fait est que les usines (et les singletons) sont extrêmement faciles à mettre en place et que les gens les utilisent beaucoup, même dans des endroits où elles ne sont pas nécessaires. Alors, quand le programmeur pense "Quels modèles de conception dois-je utiliser dans ce code?" la fabrique est la première qui lui vient à l’esprit.

De nombreuses usines sont créées parce que "Peut-être qu’un jour, je devrai créer ces classes différemment" dans l’esprit. Ce qui est une violation flagrante de YAGNI .

Et les usines deviennent obsolètes lorsque vous introduisez le framework IoC, car IoC n'est qu'une sorte d'usine. Et de nombreux frameworks IoC sont capables de créer des implémentations d'usines spécifiques.

En outre, aucun modèle de conception ne permet de créer d’énormes classes statiques avec des CreateXXXméthodes qui n’appellent que des constructeurs. Et ce n’est surtout pas appelé une usine (ni une usine abstraite).

Euphorique
la source
6
Je suis d'accord avec la plupart de vos remarques sauf "IoC est une sorte d'usine" : les conteneurs IoC ne sont pas des usines . Ils constituent une méthode pratique pour réaliser l’injection de dépendance. Oui, certains sont capables de construire des usines automatiques, mais ce ne sont pas des usines en elles-mêmes et ne devraient pas être traitées comme telles. Je soutiens également le point YAGNI. Il est utile de pouvoir substituer un test double à votre test unitaire. Refacturer tout pour fournir ceci après le fait est une douleur dans le cul . Planifiez à l'avance et ne craignez pas l'excuse "YAGNI"
AlexFoxGill Le
11
@AlexG - eh ... en pratique, pratiquement tous les conteneurs IoC fonctionnent comme des usines.
Telastyn
8
@AlexG IoC a pour objectif principal de construire des objets concrets à intégrer à d'autres objets en fonction de la configuration / convention. C'est la même chose que d'utiliser l'usine. Et vous n'avez pas besoin d'usine pour pouvoir créer une maquette pour le test. Vous venez d'instancier et de passer la maquette directement. Comme je l'ai dit dans le premier paragraphe. Les fabriques ne sont utiles que lorsque vous souhaitez transmettre la création d'une instance à l'utilisateur du module, qui appelle la fabrique.
Euphoric
5
Il y a une distinction à faire. Le modèle d’usine est utilisé par un consommateur pour créer des entités pendant l’exécution d’un programme. Un conteneur IoC est utilisé pour créer le graphe d'objets du programme lors du démarrage. Vous pouvez le faire à la main, le conteneur n’est qu’une commodité. Le consommateur d'une usine ne doit pas être au courant du conteneur IoC.
AlexFoxGill
5
Re: "Et vous n'avez pas besoin de fabrique pour créer une maquette pour test. Vous instanciez et transmettez directement la maquette" - encore une fois, dans différentes circonstances. Vous utilisez une fabrique pour demander une instance - le consommateur contrôle l’interaction. Fournir une instance via le constructeur ou une méthode ne fonctionne pas, c'est un scénario différent.
AlexFoxGill
79

La vogue du modèle Factory découle d'une croyance presque dogmatique chez les développeurs de langages de type "C" (C / C ++, C #, Java) selon laquelle l'utilisation du mot-clé "nouveau" est mauvaise et doit être évitée à tout prix (ou moins centralisé). Cela, à son tour, découle d'une interprétation ultra-stricte du principe de responsabilité unique (le "S" de SOLID), ainsi que du principe d'inversion de dépendance (le "D"). En termes simples, le SRP indique qu'idéalement, un objet de code devrait avoir une "raison de changer" et un seul; cette "raison de changer" est le but principal de cet objet, sa "responsabilité" dans la base de code, et tout ce qui nécessite une modification du code ne devrait pas nécessiter l'ouverture de ce fichier de classe. Le DIP est encore plus simple. un objet de code ne devrait jamais dépendre d'un autre objet concret,

Par exemple, en utilisant "new" et un constructeur public, vous couplez le code appelant à une méthode de construction spécifique d'une classe concrète spécifique. Votre code doit maintenant savoir qu’une classe MyFooObject existe et qu’un constructeur prend une chaîne et un int. Si ce constructeur a besoin d'informations supplémentaires, toutes les utilisations du constructeur doivent être mises à jour pour transmettre ces informations, y compris celle que vous écrivez maintenant. Par conséquent, ils doivent posséder un élément valide à transmettre. Ils doivent donc posséder: soit être modifié pour l'obtenir (en ajoutant plus de responsabilités aux objets consommateurs). En outre, si MyFooObject est remplacé dans la base de code par BetterFooObject, toutes les utilisations de l'ancienne classe doivent être modifiées pour construire le nouvel objet à la place de l'ancien.

Ainsi, au lieu de cela, tous les consommateurs de MyFooObject doivent être directement dépendants de "IFooObject", qui définit le comportement de l’implémentation de classes incluant MyFooObject. Désormais, les utilisateurs d’IFooObjects ne peuvent pas simplement construire un IFooObject (sans savoir qu’une classe concrète particulière est un IFooObject, ce dont ils n’ont pas besoin); de l'extérieur, par un autre objet qui a la responsabilité de savoir comment créer le bon IFooObject pour la circonstance, qui dans notre langage est habituellement appelée usine.

Maintenant, voici où la théorie rencontre la réalité; un objet ne peut jamais être fermé à tous les types de changement tout le temps. En l'occurrence, IFooObject est désormais un objet de code supplémentaire dans la base de code, qui doit changer chaque fois que l'interface requise par les consommateurs ou les implémentations d'IFooObjects changent. Cela introduit un nouveau niveau de complexité impliqué dans le changement de la façon dont les objets interagissent les uns avec les autres à travers cette abstraction. De plus, les consommateurs devront encore changer, et plus profondément, si l'interface elle-même est remplacée par une nouvelle.

Un bon codeur sait comment équilibrer YAGNI ("Vous n'en aurez pas besoin") avec SOLID, en analysant la conception et en recherchant les endroits susceptibles de changer, et en les restructurant pour qu'ils soient plus tolérants. ce type de changement, parce que dans ce cas , « vous êtes en avoir besoin ».

KeithS
la source
21
J'adore cette réponse, en particulier après avoir lu toutes les autres. Je peux ajouter que presque tous les (bons) nouveaux codeurs sont trop dogmatiques sur les principes par le simple fait qu'ils veulent vraiment être bons, mais n'apprennent pas encore la valeur de garder les choses simples et de ne pas abuser.
jean
1
Un autre problème avec les constructeurs publics est qu’il n’existe aucun moyen intéressant pour une classe Foode spécifier qu’un constructeur public devrait être utilisable pour la création d’ Fooinstances ou pour la création d’autres types dans le même package / assemblage , mais ne devrait pas l'être pour la création de types dérivés ailleurs. Je ne connais pas de raison particulièrement convaincante qu'un langage / framework ne puisse pas définir des constructeurs distincts pour une utilisation dans des newexpressions, par opposition à l'invocation à partir de constructeurs de sous-types, mais je ne connais aucun langage qui fasse cette distinction.
Supercat
1
Un constructeur protégé et / ou interne serait un tel signal; ce constructeur ne serait disponible que pour le code consommateur, que ce soit dans une sous-classe ou dans le même assemblage. C # ne comporte pas de combinaison de mots clés pour "protected et internal", ce qui signifie que seuls les sous-types de l'assembly peuvent l'utiliser, mais MSIL a un identificateur d'étendue pour ce type de visibilité. . Mais cela n’a vraiment pas grand-chose à voir avec l’utilisation d’usines (sauf si vous utilisez la restriction de visibilité pour imposer l’utilisation d’une usine).
KeithS
2
Réponse parfaite. Droit au but avec la partie «théorie rencontre la réalité». Il suffit de penser au nombre de milliers de développeurs et de temps humain consacrés au culte de la cargaison, à la justification, à la mise en œuvre, à leur utilisation pour ensuite tomber dans la même situation que celle que vous avez décrite, est juste exaspérant. Après YAGNI, je n'ai jamais identifié le besoin de mettre en place une usine
Breno Salgado
Je programme sur une base de code où, comme l'implémentation POO ne permet pas de surcharger le constructeur, l'utilisation de constructeurs publics est effectivement interdite. C'est déjà un problème mineur dans les langues qui le permettent. Comme créer de la température. Fahrenheit et Celsius sont tous deux des flotteurs. Vous pouvez cependant boxer ceux-ci, alors le problème est résolu.
Jgmjgm
33

Les constructeurs conviennent s'ils contiennent un code court et simple.

Lorsque l'initialisation devient plus que l'attribution de quelques variables aux champs, une fabrique a du sens. Voici certains des avantages:

  • Un code long et compliqué a plus de sens dans une classe dédiée (une usine). Si le même code est placé dans un constructeur qui appelle un tas de méthodes statiques, cela va polluer la classe principale.

  • Dans certaines langues et dans certains cas, lancer des exceptions dans les constructeurs est une très mauvaise idée , car cela peut introduire des bogues.

  • Lorsque vous appelez un constructeur, vous, l'appelant, devez connaître le type exact de l'instance que vous souhaitez créer. Ce n'est pas toujours le cas (en tant que Feeder, j'ai juste besoin de construire le Animalpour pouvoir le nourrir; je me fiche de savoir si c'est un Dogou a Cat).

Arseni Mourzenko
la source
2
Les choix ne sont pas seulement "usine" ou "constructeur". Un Feederpeut n'utiliser ni l'un ni l'autre et appeler à la place Kennella getHungryAnimalméthode de son objet .
DougM
4
+1 Je trouve qu'adhérer à une règle d'absence absolue de logique dans un constructeur n'est pas une mauvaise idée. Un constructeur ne doit être utilisé que pour définir l'état initial de l'objet en affectant ses valeurs d'argument à des variables d'instance. Si quelque chose de plus compliqué est nécessaire, créez au minimum une méthode factory (classe) pour construire l'instance.
KaptajnKold
C’est la seule réponse satisfaisante que j’ai vue ici qui m’a évité d’écrire la mienne. Les autres réponses ne traitent que de concepts abstraits.
TheCatWhisperer
Mais cet argument peut également être considéré comme valable Builder Pattern. N'est-ce pas?
soufrk
Règle des constructeurs
Breno Salgado
10

Si vous travaillez avec des interfaces, vous pouvez rester indépendant de la mise en œuvre réelle. Une usine peut être configurée (via des propriétés, des paramètres ou une autre méthode) pour instancier une ou plusieurs implémentations différentes.

Un exemple simple: vous voulez communiquer avec un appareil mais vous ne savez pas si ce sera via Ethernet, COM ou USB. Vous définissez une interface et 3 implémentations. Au moment de l'exécution, vous pouvez alors sélectionner la méthode de votre choix et l'usine vous donnera la mise en œuvre appropriée.

Utilisez-le souvent ...

Paul
la source
5
J'ajouterais qu'il est bon de l'utiliser lorsqu'il y a plusieurs implémentations d'une interface et que le code d'appel ne sait pas ou ne devrait pas savoir lequel choisir. Mais lorsqu'une méthode factory est simplement un wrapper statique autour d'un constructeur unique, comme dans la question, c'est l'anti-motif. Il doit y avoir plusieurs implémentations parmi lesquelles choisir, sinon l'usine s'interposera et ajoutera une complexité inutile.
Maintenant, vous avez la flexibilité supplémentaire et en théorie votre application peut utiliser Ethernet, COM, USB et série, est prête pour Fireloop ou autre, en théorie. En réalité, votre application ne communiquera que par Ethernet .... jamais.
Pieter B
7

C'est le symptôme d'une limitation dans les systèmes de modules de Java / C #.

En principe, il n'y a aucune raison pour que vous ne puissiez pas échanger une implémentation d'une classe contre une autre avec les mêmes signatures de constructeur et de méthode. Il y a des langues qui permettent cela. Cependant, Java et C # insistent sur le fait que chaque classe a un identifiant unique (le nom complet) et que le code client se termine par une dépendance codée en dur.

Vous pouvez en quelque sorte contourner ce problème en manipulant le système de fichiers et les options du compilateur afin de les com.example.Foomapper vers un fichier différent, mais cela est surprenant et peu intuitif. Même si vous le faites, votre code est toujours lié à une seule implémentation de la classe. En d'autres termes, si vous écrivez une classe Fooqui en dépend MySet, vous pouvez choisir une implémentation MySetau moment de la compilation, mais vous ne pouvez toujours pas instancier Foos à l'aide de deux implémentations différentes de MySet.

Cette décision malheureuse en matière de conception oblige les utilisateurs à utiliser interfaceinutilement leur système pour protéger leur code contre la possibilité qu’ils aient besoin ultérieurement d’une implémentation différente de quelque chose ou pour faciliter les tests unitaires. Ce n'est pas toujours faisable. si vous avez des méthodes qui examinent les champs privés de deux instances de la classe, vous ne pourrez pas les implémenter dans une interface. C'est pourquoi, par exemple, vous ne voyez pas unionl' Setinterface de Java . Néanmoins, en dehors des types et des collections numériques, les méthodes binaires ne sont pas courantes, vous pouvez donc vous en tirer.

Bien entendu, si vous appelez, vous dépendez new Foo(...)toujours de la classe . Vous avez donc besoin d'une fabrique si vous souhaitez qu'une classe puisse instancier directement une interface. Cependant, il est généralement préférable d’accepter l’instance dans le constructeur et de laisser à une autre personne le soin de choisir l’implémentation à utiliser.

C'est à vous de décider s'il vaut la peine de gonfler votre base de code avec des interfaces et des usines. D'une part, si la classe en question est interne à votre base de code, le refactoriser le code afin qu'il utilise une classe ou une interface différente dans le futur est trivial; vous pouvez invoquer YAGNI et le refactor ultérieurement si la situation se présente. Mais si la classe fait partie de l'API publique d'une bibliothèque que vous avez publiée, vous n'avez pas la possibilité de corriger le code client. Si vous n'utilisez pas interfaceet que vous avez besoin de plusieurs implémentations plus tard, vous serez coincé entre le marteau et l'enclume.

Doval
la source
2
Je souhaite que Java et ses dérivés, tels que .NET, possèdent une syntaxe spéciale pour une classe créant ses instances, sans newquoi il ne s'agirait que du sucre syntaxique pour appeler une méthode statique nommée de manière spécifique (qui serait générée automatiquement si un type avait un "constructeur public". "mais n’a pas explicitement inclus la méthode). IMHO, si le code veut juste une chose par défaut ennuyeuse à implémenter List, il devrait être possible pour l'interface de lui en donner une sans que le client ait à connaître une implémentation particulière (par exemple ArrayList).
Supercat
2

À mon avis, ils utilisent simplement Simple Factory, qui n'est pas un modèle de conception approprié et ne doit pas être confondu avec Abstract Factory ou la méthode Factory.

Et comme ils ont créé une "classe de tissu énorme avec des milliers de méthodes statiques CreateXXX", cela ressemble à un anti-motif (une classe de Dieu peut-être?).

Je pense que les méthodes Simple Factory et stator creator (qui ne nécessitent pas de classe externe) peuvent être utiles dans certains cas. Par exemple, lorsque la construction d'un objet nécessite diverses étapes, telles que l'instanciation d'autres objets (par exemple, favoriser la composition).

Je n'appellerais même pas cela une Factory, mais juste un tas de méthodes encapsulées dans une classe aléatoire avec le suffixe "Factory".

FranMowinckel
la source
1
Simple Factory a sa place. Imaginons qu'une entité prenne deux paramètres de constructeur, int xet IFooService fooService. Vous ne voulez pas être en train de passer fooServicepartout, vous créez donc une usine avec une méthode Create(int x)et injectez le service à l'intérieur de l'usine.
AlexFoxGill
4
@ AlexG Et puis, vous devez circuler IFactorypartout au lieu de IFooService.
Euphoric
3
Je suis d'accord avec Euphoric; D'après mon expérience, les objets qui sont injectés dans un graphique à partir du haut ont tendance à être d'un type dont tous les objets de niveau inférieur ont besoin. Par conséquent, passer à IFooServices n'est pas grave. Remplacer une abstraction par une autre ne fait rien, mais obscurcit davantage la base de code.
KeithS
cela semble totalement hors de propos de la question posée: "Pourquoi devrais-je utiliser une classe d'usine au lieu d'une construction d'objet directe? Quand devrais-je utiliser des constructeurs publics et quand devrais-je utiliser des usines?" Voir Comment répondre
Moucheron
1
Ce n'est que le titre de la question, je pense que vous avez manqué le reste. Voir le dernier point du lien fourni;).
FranMowinckel
1

En tant qu'utilisateur d'une bibliothèque, si la bibliothèque a des méthodes d'usine, vous devez les utiliser. Vous supposeriez que la méthode de fabrication donne à l'auteur de la bibliothèque la possibilité d'effectuer certaines modifications sans affecter votre code. Ils peuvent par exemple renvoyer une instance d'une sous-classe dans une méthode factory, ce qui ne fonctionnerait pas avec un simple constructeur.

En tant que créateur d'une bibliothèque, vous utiliseriez des méthodes d'usine si vous souhaitez utiliser vous-même cette flexibilité.

Dans le cas que vous décrivez, vous semblez avoir l’impression que le remplacement des constructeurs par des méthodes d’usine était inutile. C'était certainement une douleur pour tout le monde impliqué; une bibliothèque ne doit rien enlever de son API sans une très bonne raison. Donc, si j'avais ajouté les méthodes d'usine, j'aurais laissé les constructeurs existants disponibles, peut-être obsolètes, jusqu'à ce qu'une méthode d'usine n'appelle plus ce constructeur et que le code utilisant le constructeur brut fonctionne moins bien qu'il ne le devrait. Votre impression pourrait très bien être juste.

gnasher729
la source
Notez également; Si le développeur doit fournir une classe dérivée avec des fonctionnalités supplémentaires (propriétés supplémentaires, initialisation), il peut le faire. (en supposant que les méthodes d'usine sont écrasables). L’auteur de l’API peut également fournir une solution de contournement aux besoins particuliers des clients.
Eric Schneider
-4

Cela semble être obsolète à l'âge de Scala et de la programmation fonctionnelle. Une base solide de fonctions remplace environ un million de classes.

A noter également que le double de Java {{ne fonctionne plus avec une usine c'est-à-dire

someFunction(new someObject() {{
    setSomeParam(...);
    etc..
}})

Ce qui peut vous permettre de créer une classe anonyme et de la personnaliser.

Dans le dilemme espace-temps, le facteur temps est maintenant tellement réduit grâce aux processeurs rapides que la programmation fonctionnelle permettant de réduire l'espace, c'est-à-dire la taille du code, est désormais pratique.

Marc
la source
2
cela ressemble plus à un commentaire tangentiel (voir Comment répondre ), et il ne semble rien offrir de substantiel sur les points évoqués et expliqués dans les 9 réponses précédentes
gnat