web-dev-qa-db-fra.com

Dois-je utiliser le Factory Pattern lors de l'instanciation d'objets avec des constructeurs très différents?

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?

6
Craig

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.

17
Doc Brown

Classe d'usine

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....

Méthode d'usine générique

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 });
7
John Wu

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.

2
Nick Alexeev