Disons (juste à titre d'exemple) que j'ai trois classes qui implémentent IShape. L'un est un carré avec un constructeur de carré (longueur int). Le deuxième est un Triangle avec un constructeur de Triangle (base int, hauteur int). Le troisième est un cercle avec un constructeur de cercle (double rayon).
Étant donné que toutes les classes partagent la même interface, mon esprit va au modèle d'usine en tant que modèle de création à utiliser. Mais, la méthode d'usine serait maladroite car elle doit fournir des paramètres pour ces différents constructeurs - par exemple:
IShape CreateShape(int length, int base, int height, double radius)
{
...
return new Circle(radius);
...
return new Triage(base, height);
...
return new Square(length);
}
Cette méthode d'usine semble assez maladroite. Est-ce là où une usine abstraite ou un autre modèle de conception entre en jeu comme approche supérieure?
Vous avez une solution à la recherche d'un problème, c'est pourquoi vous rencontrez des problèmes.
Une méthode d'usine n'est pas une fin en soi, c'est un moyen pour une fin. Vous devez donc commencer à identifier le problème que vous souhaitez résoudre en premier , ce qui signifie que vous avez besoin d'un cas d'utilisation pour construire ces objets, en vous fournissant le contexte nécessaire. Comme:
vous avez une source de données externe comme un flux de fichiers ou une base de données avec des descriptions d'objets
vous voulez qu'une fabrique crée des objets IShape
à partir de cette source de données (donc avoir un et un seul endroit dans le code à modifier au cas où la liste des formes serait étendue)
Dans le contexte du "flux de fichiers", par exemple, une méthode d'usine CreateShape
pourrait probablement obtenir une chaîne en tant que paramètre, contenant une description d'objet (peut-être une chaîne CSV, une chaîne JSON ou un extrait XML), et le l'exigence serait d'analyser cette chaîne pour créer le bon objet:
IShape CreateShape(string shapeDescription)
{
switch(getShapeType(shapeDescription))
{
case "Circle":
radius=parseRadius(shapeDescription);
return new Circle(radius);
case "Triangle":
base=parseBase(shapeDescription);
height=parseHeight(shapeDescription);
return new Triangle(base, height);
...
}
Maintenant, la liste des paramètres de cette méthode ne semble plus si maladroite, je suppose?
Autres cas d'utilisation potentiels:
les formes sont créées en fonction des entrées utilisateur: l'usine obtient une partie des données d'entrée utilisateur en tant que paramètre
création de formes basées sur une logique métier dynamique
Vous devez également prendre en compte d'autres exigences non fonctionnelles:
voulez-vous que votre usine vous aide à vous déconnecter de cette source de données externe? Par exemple, pour les tests unitaires? Ensuite, faites-en non seulement une méthode, faites-en une classe avec une interface, qui peut être simulée.
voulez-vous que l'usine elle-même soit un composant réutilisable, selon le principe Open/Closed, où le code n'a pas à être touché même lorsque de nouvelles formes doivent être ajoutées? Ensuite, vous devez le construire de manière plus générique, en utilisant la réflexion, les génériques, le modèle de prototype ou le modèle de stratégie .
Et oui, pour certains cas d'utilisation, vous n'aurez probablement pas besoin du tout de méthode d'usine.
Donc en bref: clarifiez d'abord vos besoins . Si vous ne connaissez pas le contexte d'utilisation de la méthode d'usine, vous n'en avez pas encore besoin.
Utilisez une fabrique classe, qui peut avoir plusieurs méthodes. L'usine devrait avoir sa propre interface.
interface IShapeFactory
{
IShape CreateRectangle(float width, float height);
IShape CreateCircle(float radius);
}
class ShapeFactory : IShapeFactory
{
///etc....
Si vous préférez vous en tenir à une usine méthode, et que vous souhaitez paramétrer le type à l'aide d'un paramètre de type générique, vous avez un peu de travail à faire pour rendre les entrées génériques également.
L'astuce consiste à définir une interface pour les paramètres d'entrée (par exemple IShapeArgsFor<T>
). Parce que l'interface est liée à T
, le compilateur peut déduire le reste:
T CreateShape<T>(IShapeArgsFor<T> input) where T : IShape
Supporté par
interface IShapeArgsFor<T> where T : IShape
{
}
Et
class CircleArgs : IShapeArgsFor<Circle>
{
public float Radius { get; }
}
class RectangleArgs : IShapeArgsFor<Rectangle>
{
public float Height { get; }
public float Width { get; }
}
etc....
Vous l'appeleriez alors comme ceci:
var circle = CreateShape(new CircleArgs { Radius = 3 });
Cette méthode d'usine semble assez maladroite.
Le code client qui appelle l'usine doit passer des paramètres pour toutes les formes possibles. De plus, les paramètres qui ne s'appliquent pas à la forme souhaitée doivent être tronqués. Pour obtenir un triangle, l'appel serait
CreateShape(length: 0, base: 21, height: 42, radius: 0) // returns a triangle
// 0 or a negative number is a special value
Comment le code appelant connaîtrait-il les paramètres à supprimer?
Il y a deux options:
Le code appelant ne sait pas. Il récupère les données de forme quelque part et les transmet à l'usine. Les données de forme entrantes ont déjà tous les stubs nécessaires.
Ceci est un scénario valide.
Le code appelant ajoute les talons. En effet, le code appelant devrait "savoir" quelle forme il veut (sinon il ne sait pas quels paramètres sont inutiles).
Cela irait à l'encontre du but de l'usine.