Les classes de base comme usines?

14

J'écrivais du code au cours du week-end et je me suis retrouvé à vouloir écrire une usine comme méthode statique dans une classe de base.

Ma question est simplement de savoir s'il s'agit d'une approche ac # idomatique?

Mon sentiment que cela pourrait ne pas provenir du fait que la classe de base a connaissance de la classe dérivée.

Cela dit, je ne suis pas sûr d'un moyen plus simple d'obtenir le même résultat. Une toute autre classe d'usine semble (du moins pour moi) comme une complexité inutile (?)

Quelque chose comme:

class Animal
{
  public static Animal CreateAnimal(string name)
  {
     switch(name)
     {
        case "Shark":
          return new SeaAnimal();
          break;
        case "Dog":
          return new LandAnimal();
          break;
        default:
          throw new Exception("unknown animal");
     }
  }
}
class LandAnimal : Animal
{
}

class SeaAnimal : Animal
{
}
Aaron Anodide
la source
Comment testerez-vous vos usines?
avec le code dans cette question, je ne le ferais pas. mais la réponse m'a aidé à avancer sur la voie de l'intégration de plus de tests dans mon style de codage
Aaron Anodide
Considérez que, après avoir attiré à la fois le Seaworld et le Landworld dans votre classe d'animaux, dans l'ensemble, il est plus difficile à gérer dans un cadre isolé.

Réponses:

13

Eh bien, l'avantage d'une classe d'usine distincte est qu'elle peut être simulée lors de tests unitaires.

Mais si vous n'allez pas le faire, ou le rendre polymorphe d'une autre manière, alors une méthode Factory statique sur la classe elle-même est OK.

pdr
la source
merci, pourriez-vous donner un exemple de ce à quoi vous faites référence lorsque vous dites polymorphe d'une autre manière?
Aaron Anodide
Je veux dire que si jamais vous voulez pouvoir remplacer une méthode d'usine par une autre. Le moquer à des fins de test n'est qu'un des exemples les plus courants de cela.
pdr
18

Vous pouvez utiliser Generics pour éviter l'instruction switch et dissocier également les implémentations en aval de la classe de base:

 public static T CreateAnimal<T>() where T: new, Animal
 {
    return new T();
 }

Usage:

LandAnimal rabbit = Animal.CreateAnimal();  //Type inference should should just figure out the T by the return indicated.

ou

 var rabbit = Animal.CreateAnimal<LandAnimal>(); 
Nick Nieslanik
la source
2
@PeterK. Fowler fait référence aux instructions switch dans Code Smells, mais sa solution est qu'elles doivent être extraites vers les classes Factory, plutôt que d'être remplacées. Cela dit, si vous voulez toujours connaître le type au moment du développement, les génériques sont une solution encore meilleure. Mais si vous ne le faites pas (comme le suggère l'exemple d'origine), je dirais que l'utilisation de la réflexion pour éviter un changement dans une méthode d'usine peut être une fausse économie.
pdr
les instructions switch et les chaînes if-else-if sont identiques. J'utilise l'ancien en raison de la vérification du temps de compilation qui force le cas par défaut à être traité
Aaron Anodide
4
@PeterK, dans une instruction switch, le code est en un seul endroit. Dans OO à part entière, le même code peut être réparti sur plusieurs classes distinctes. Il y a une zone grise où la complexité supplémentaire d'avoir de nombreux extraits est moins souhaitable qu'une instruction switch.
@Thorbjorn, j'ai eu cette pensée exacte, mais jamais aussi succinctement, merci d'avoir mis des mots
Aaron Anodide
5

Poussée à l'extrême, l'usine pourrait également être générique.

interface IFactory<K, T> where K : IComparable
{
    T Create(K key);
}

Ensuite, on peut créer n'importe quel type de fabrique d'objets, qui à son tour pourrait créer tout type d'objet. Je ne sais pas si c'est plus simple, c'est certainement plus générique.

Je ne vois rien de mal à une instruction switch pour une petite implémentation d'usine. Une fois que vous êtes dans un grand nombre d'objets ou dans différentes hiérarchies de classes possibles d'objets, je pense qu'une approche plus générique est mieux adaptée.

Jon Raynor
la source
1

Mauvaise idée. Premièrement, il viole le principe ouvert-fermé. Pour tout nouvel animal, vous devrez à nouveau jouer avec votre classe de base et vous pourriez potentiellement le casser. Les dépendances iraient dans le mauvais sens.

Si vous devez créer des animaux à partir d'une configuration, une construction comme celle-ci serait en quelque sorte OK, bien que l'utilisation de la réflexion pour obtenir le type qui correspond au nom et l'instancier en utilisant les informations de type obtenues serait une meilleure option.

Mais vous devez quand même créer une classe d'usine dédiée, non liée à la hiérarchie des classes animales, et renvoyer une interface IAnimal plutôt qu'un type de base. Ensuite, cela deviendrait utile, vous auriez réalisé un découplage.

Martin Maat
la source
0

Cette question est opportune pour moi - j'écrivais presque exactement ce code hier. Remplacez simplement "Animal" par tout ce qui est pertinent dans mon projet, bien que je m'en tienne à "Animal" pour les besoins de la discussion ici. Au lieu d'une instruction «switch», j'avais une série un peu plus complexe d'instructions «if», impliquant plus que la simple comparaison d'une variable à certaines valeurs fixes. Mais c'est un détail. Une méthode d'usine statique semblait être un bon moyen de concevoir des choses, car la conception est née de la refactorisation de désordres de code rapides et sales.

J'ai rejeté cette conception au motif que la classe de base connaissait la classe dérivée. Si les classes LandAnimal et SeaAnimal sont petites, soignées et faciles, elles peuvent être dans le même fichier source. Mais j'ai de grandes méthodes compliquées pour lire des fichiers texte non conformes à des normes officiellement définies - je veux ma classe LandAnimal dans son propre fichier source.

Cela conduit à une dépendance de fichier circulaire - LandAnimal est dérivé d'Animal, mais Animal doit déjà savoir que LandAnimal, SeaAnimal et quinze autres classes existent. J'ai retiré la méthode d'usine, je l'ai placée dans son propre fichier (dans mon application principale, pas dans ma bibliothèque Animals). Avoir une méthode d'usine statique semblait mignon et intelligent, mais j'ai réalisé qu'elle ne résolvait en fait aucun problème de conception.

Je n'ai aucune idée de la façon dont cela se rapporte au C # idiomatique, car je change beaucoup de langage, j'ignore généralement les idiomes et les conventions propres aux langages et les piles de développement en dehors de mon travail habituel. Si quoi que ce soit, mon C # pourrait sembler "pythonique" si c'est significatif. Je vise la clarté générale.

De plus, je ne sais pas s'il y a un avantage à utiliser la méthode d'usine statique dans le cas de petites classes simples qui ne seront probablement pas étendues dans les travaux futurs - le tout dans un fichier source peut être bien dans certains cas.

DarenW
la source