bonnes pratiques de la fonction usine python

30

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

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

Maintenant, je veux ajouter une fonction qui crée un Fooobjet 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)
Jason S
la source

Réponses:

45

Le choix devrait plutôt être entre une fonction d'usine et une troisième option; 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 Fooet la méthode de fabrique 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 l'usine va produire, sans avoir à le rendre explicite dans le nom de la fonction. Cela devient beaucoup plus clair lorsque vous disposez non seulement de plusieurs méthodes d'usine, mais également de plusieurs classes.

Comparez les deux alternatives suivantes:

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

contre.

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

Encore mieux, votre utilisateur final pourrait importer uniquement la Fooclasse 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 datetimemodule stdlib utilise largement les usines de méthodes de classe, et son API est beaucoup plus claire pour cela.

Martijn Pieters
la source
Très bonne réponse. Pour en savoir plus @classmethod, voir Signification de @classmethod et @staticmethod pour débutant?
nealmcb
Grande concentration sur la fonctionnalité de l'utilisateur final
drtf
1

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 Fooimplé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être construit. Ensuite, je créerais un Fooen 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.

mdeff
la source