web-dev-qa-db-fra.com

python meilleures pratiques de fonction d'usine

Supposons que j'ai un fichier foo.py contenant une classe Foo:

class Foo(object):
   def __init__(self, data):
      ...

Maintenant, je veux ajouter une fonction qui crée un objet Foo d'une certaine manière à partir de données source brutes. Dois-je le mettre comme méthode statique dans Foo ou comme une autre fonction distincte?

class Foo(object):
   def __init__(self, data):
      ...
# option 1:
   @staticmethod
   def fromSourceData(sourceData):
      return Foo(processData(sourceData))

# option 2:
def makeFoo(sourceData):
   return Foo(processData(sourceData))

Je ne sais pas s'il est plus important d'être pratique pour les utilisateurs:

foo1 = foo.makeFoo(sourceData)

ou s'il est plus important de maintenir un couplage clair entre la méthode et la classe:

foo1 = foo.Foo.fromSourceData(sourceData)
32
Jason S

Le choix devrait être entre une fonction d'usine et une troisième option à la place; la méthode de classe:

class Foo(object):
   def __init__(self, data):
      ...

   @classmethod
   def fromSourceData(klass, sourceData):
      return klass(processData(sourceData))

Les usines Classmethod ont l'avantage supplémentaire d'être héritables. Vous pouvez maintenant créer une sous-classe de Foo, et la méthode d'usine héritée produirait alors des instances de cette sous-classe.

Avec un choix entre une fonction et une méthode de classe, je choisirais la méthode de classe. Il documente assez clairement quel type d'objet que l'usine va produire, sans avoir à le rendre explicite dans le nom de la fonction. Cela devient beaucoup plus clair lorsque vous avez non seulement plusieurs méthodes d'usine, mais également plusieurs classes.

Comparez les deux alternatives suivantes:

foo.Foo.fromFrobnar(...)
foo.Foo.fromSpamNEggs(...)
foo.Foo.someComputedVersion()
foo.Bar.fromFrobnar(...)
foo.Bar.someComputedVersion()

vs.

foo.createFooFromFrobnar()
foo.createFooFromSpamNEggs()
foo.createSomeComputedVersionFoo()
foo.createBarFromFrobnar()
foo.createSomeComputedVersionBar()

Encore mieux, votre utilisateur final pourrait importer uniquement la classe Foo et l'utiliser pour les différentes façons de la créer, car vous avez toutes les méthodes d'usine en un seul endroit:

from foo import Foo
Foo()
Foo.fromFrobnar(...)
Foo.fromSpamNEggs(...)
Foo.someComputedVersion()

Le module stdlib datetime utilise largement les usines de méthodes de classe, et son API est beaucoup plus claire pour cela.

46
Martijn Pieters

En Python, je préfère généralement une hiérarchie de classes aux méthodes d'usine. Quelque chose comme:

class Foo(object):
    def __init__(self, data):
        pass

    def method(self, param):
        pass

class SpecialFoo(Foo):
    def __init__(self, param1, param2):
        # Some processing.
        super().__init__(data)

class FromFile(Foo):
    def __init__(self, path):
        # Some processing.
        super().__init__(data)

Je vais mettre toute la hiérarchie (et seulement celle-ci) dans un module, disons foos.py. La classe de base Foo implémente généralement toutes les méthodes (et en tant que telle, l'interface) et n'est généralement pas construite directement par les utilisateurs. Les sous-classes sont un moyen d'écraser le constructeur de base et de spécifier comment Foo doit être construit. Ensuite, je créerais un Foo en appelant le constructeur approprié, comme:

foo1 = mypackage.foos.SpecialFoo(param1, param2)
foo2 = mypackage.foos.FromFile('/path/to/foo')

Je le trouve plus élégant et flexible que les usines construites à partir de méthodes statiques, de méthodes de classe ou de fonctions de niveau module.

1
mdeff