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 CreateXXX
mé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?
Réponses:
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:
Considérez cette situation.
Assemblée A (-> signifie dépend de):
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:
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
new
mot - clé.la source
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
CreateXXX
méthodes qui n’appellent que des constructeurs. Et ce n’est surtout pas appelé une usine (ni une usine abstraite).la source
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 ».
la source
Foo
de spécifier qu’un constructeur public devrait être utilisable pour la création d’Foo
instances 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 desnew
expressions, par opposition à l'invocation à partir de constructeurs de sous-types, mais je ne connais aucun langage qui fasse cette distinction.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 leAnimal
pour pouvoir le nourrir; je me fiche de savoir si c'est unDog
ou aCat
).la source
Feeder
peut n'utiliser ni l'un ni l'autre et appeler à la placeKennel
lagetHungryAnimal
méthode de son objet .Builder Pattern
. N'est-ce pas?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 ...
la source
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.Foo
mapper 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 classeFoo
qui en dépendMySet
, vous pouvez choisir une implémentationMySet
au moment de la compilation, mais vous ne pouvez toujours pas instancierFoo
s à l'aide de deux implémentations différentes deMySet
.Cette décision malheureuse en matière de conception oblige les utilisateurs à utiliser
interface
inutilement 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 pasunion
l'Set
interface 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
interface
et que vous avez besoin de plusieurs implémentations plus tard, vous serez coincé entre le marteau et l'enclume.la source
new
quoi 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émenterList
, 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 exempleArrayList
).À 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".
la source
int x
etIFooService fooService
. Vous ne voulez pas être en train de passerfooService
partout, vous créez donc une usine avec une méthodeCreate(int x)
et injectez le service à l'intérieur de l'usine.IFactory
partout au lieu deIFooService
.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.
la source
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-à-direCe 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.
la source