J'ai vu l'historique de plusieurs projets de bibliothèque de classes С # et Java sur GitHub et CodePlex, et je vois une tendance à passer aux classes d'usine par opposition à l'instanciation d'objet directe.
Pourquoi devrais-je utiliser intensivement les classes d'usine? J'ai une assez bonne bibliothèque, où les objets sont créés à l'ancienne - en invoquant des constructeurs publics de classes. Dans les derniers commits, les auteurs ont rapidement changé tous les constructeurs publics de milliers de classes en internes, et ont également créé une énorme classe d'usine avec des milliers de méthodes statiques CreateXXX
qui renvoient simplement de nouveaux objets en appelant les constructeurs internes des Des classes. L'API du projet externe est cassée, bravo.
Pourquoi un tel changement serait-il utile? Quel est l'intérêt de refactoriser de cette manière? Quels sont les avantages de remplacer les appels aux constructeurs de classe publique par des appels de méthode d'usine statique?
Quand dois-je utiliser des constructeurs publics et quand dois-je utiliser des usines?
Les classes d'usine sont souvent implémentées car elles permettent au projet de suivre de plus près les principes SOLIDES . En particulier, les principes de ségrégation d'interface et d'inversion de dépendance.
Les usines et les interfaces permettent une flexibilité beaucoup plus longue. Il permet une conception plus découplée - et donc plus testable -. Voici une liste non exhaustive des raisons pour lesquelles vous pourriez emprunter cette voie:
Considérez cette situation.
Assemblage A (-> les moyens dépendent):
Class A -> Class B
Class A -> Class C
Class B -> Class D
Je veux déplacer la classe B vers l'assembly B, qui dépend de l'assembly A. Avec ces dépendances concrètes, je dois déplacer la majeure partie de ma hiérarchie de classe entière. Si j'utilise des interfaces, je peux éviter une grande partie de la 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 déplacer la classe B vers l'Assemblée 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 offre 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.
Le respect du principe de ségrégation d'interface et du principe d'inversion de dépendance nous permet de créer des applications découplées très flexibles. Une fois que vous avez travaillé sur l'un de ces types d'applications, vous ne voudrez plus jamais recommencer à utiliser le mot clé new
.
Comme whatsisname a dit, je crois que c'est le cas de cargo cult conception de logiciels. 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 seule instance et vous pouvez simplement passer 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 œuvre et donc les gens les utilisent beaucoup, même dans des endroits où ils ne sont pas nécessaires. Ainsi, lorsque le programmeur pense "Quels modèles de conception dois-je utiliser dans ce code?" l'usine est la première qui lui vient à l'esprit.
De nombreuses usines sont créées parce que "Peut-être qu'un jour, j'aurai besoin de créer ces classes différemment" à l'esprit. Ce qui est clairement une violation de YAGNI .
Et les usines deviennent obsolètes lorsque vous introduisez le cadre IoC, car l'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.
De plus, aucun modèle de conception ne dit de créer d'énormes classes statiques avec des méthodes CreateXXX
qui appellent uniquement des constructeurs. Et ce n'est surtout pas appelé Factory (ni Abstract Factory).
La vogue du modèle Factory découle d'une croyance presque dogmatique parmi les codeurs des langages de "style C" (C/C++, C #, Java) selon laquelle l'utilisation du "nouveau" mot-clé est mauvaise et doit être évitée à tout prix (ou moins centralisée). Ceci, à son tour, provient 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 dit que, idéalement, un objet de code devrait avoir une "raison de changer", et une seule; 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, mais plutôt d'une abstraction.
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 possède un constructeur qui prend une chaîne et un int. Si ce constructeur a besoin de plus d'informations, toutes les utilisations du constructeur doivent être mises à jour pour transmettre ces informations, y compris celle que vous écrivez maintenant, et par conséquent, ils doivent avoir quelque chose de valide à transmettre, et ils doivent donc soit avoir ou être modifié pour l'obtenir (en ajoutant plus de responsabilités aux objets consommateurs). De plus, si MyFooObject est jamais remplacé dans la base de code par BetterFooObject, toutes les utilisations de l'ancienne classe doivent changer pour construire le nouvel objet au lieu de l'ancien.
Ainsi, à la place, tous les consommateurs de MyFooObject devraient dépendre directement de "IFooObject", qui définit le comportement de l'implémentation des classes, y compris MyFooObject. Maintenant, les consommateurs d'IFooObjects ne peuvent pas simplement construire un IFooObject (sans savoir qu'une classe concrète particulière est un IFooObject, dont ils n'ont pas besoin), donc à la place, ils doivent recevoir une instance d'une classe ou d'une méthode implémentant IFooObject 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 généralement connu comme une usine.
Maintenant, voici où la théorie rencontre la réalité; un objet peut jamais être fermé à tout type de changement tout le temps. Par exemple, IFooObject est maintenant 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 change. Cela introduit un nouveau niveau de complexité impliqué dans le changement de la façon dont les objets interagissent entre eux à 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 ("You Ain't Gonna Need It") avec SOLID, en analysant la conception et en trouvant des endroits qui sont très susceptibles de devoir changer d'une manière particulière, et en les refactorisant pour être plus tolérants envers ce type de changement, parce que dans ce cas "vous êtes vous en aurez besoin".
Les constructeurs sont très bien lorsqu'ils contiennent du code court et simple.
Lorsque l'initialisation devient plus que l'attribution de quelques variables aux champs, une fabrique a du sens. Voici quelques 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 polluera la classe principale.
Dans certains langages 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 Animal
pour le nourrir; peu m'importe si c'est un Dog
ou un Cat
).
Si vous travaillez avec des interfaces, vous pouvez rester indépendant de l'implémentation réelle. Une fabrique peut être configurée (via les propriétés, les paramètres ou une autre méthode) pour instancier une parmi 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 que vous souhaitez et l'usine vous donnera l'implémentation appropriée.
Utilisez-le souvent ...
C'est le symptôme d'une limitation dans les systèmes de modules Java/C #.
En principe, il n'y a aucune raison pour que vous ne puissiez pas échanger une implémentation d'une classe pour une autre avec les mêmes signatures de constructeur et de méthode. Il y a langues qui le permettent. Cependant, Java et C # insistent pour que chaque classe ait un identifiant unique (le nom complet) et le code client se retrouve avec une dépendance codée en dur sur lui.
Vous pouvez sorte de contourner ce problème en manipulant le système de fichiers et les options du compilateur afin que com.example.Foo
Mappe 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. C'est à dire. Si vous écrivez une classe Foo
qui dépend d'une classe MySet
, vous pouvez choisir une implémentation de MySet
au moment de la compilation mais vous ne pouvez toujours pas instancier Foo
s utilisant deux implémentations différentes de MySet
.
Cette décision de conception malheureuse oblige les gens à utiliser inutilement interface
s pour pérenniser leur code contre la possibilité qu'ils auront besoin plus tard 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 regardent 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 union
dans l'interface Set
de Java. Pourtant, en dehors des types et des collections numériques, les méthodes binaires ne sont pas courantes, vous pouvez donc généralement vous en tirer.
Bien sûr, si vous appelez new Foo(...)
vous avez toujours une dépendance sur la classe , donc vous avez besoin d'une fabrique si vous voulez une classe pour pouvoir instancier directement une interface. Cependant, il est généralement préférable d'accepter l'instance dans le constructeur et de laisser quelqu'un d'autre décider de 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, la refactorisation du code afin qu'il utilise une classe ou une interface différente à l'avenir est triviale; vous pouvez invoquer YAGNI et refactoriser plus tard 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 de interface
et que vous avez besoin ultérieurement de plusieurs implémentations, vous serez coincé entre un rocher et un endroit dur.
À mon avis, ils utilisent simplement la Simple Factory, qui n'est pas un modèle de conception approprié et ne doit pas être confondu avec la Abstract Factory ou la méthode Factory.
Et comme ils ont créé une "énorme classe de tissus avec des milliers de méthodes statiques CreateXXX", cela sonne comme un anti-pattern (une classe God peut-être?).
Je pense que la Simple Factory et les méthodes de création statique (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 comme 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".
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 d'usine donne à l'auteur de la bibliothèque la flexibilité d'effectuer certaines modifications sans affecter votre code. Ils pourraient par exemple retourner une instance d'une sous-classe dans une méthode d'usine, 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 cette flexibilité vous-même.
Dans le cas que vous décrivez, vous semblez avoir l'impression que remplacer les constructeurs par des méthodes d'usine était tout simplement inutile. Ce fut certainement une douleur pour toutes les personnes impliquées; une bibliothèque ne doit rien supprimer de son API sans une très bonne raison. Donc, si j'avais ajouté des 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 simplement ce constructeur et le code utilisant le constructeur ordinaire fonctionne moins bien qu'il ne devrait . Votre impression pourrait très bien avoir raison.