best practice per la funzione factory di Python


30

Supponiamo di avere un file foo.pycontenente una classe Foo:

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

Ora voglio aggiungere una funzione che crea un Foooggetto in un certo modo dai dati di origine grezzi. Dovrei metterlo come metodo statico in Foo o come un'altra funzione separata?

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

Non so se sia più importante essere convenienti per gli utenti:

foo1 = foo.makeFoo(sourceData)

o se è più importante mantenere un chiaro accoppiamento tra il metodo e la classe:

foo1 = foo.Foo.fromSourceData(sourceData)

Risposte:


45

La scelta dovrebbe invece tra una funzione di fabbrica e una terza opzione; il metodo di classe:

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

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

Le fabbriche Classmethod hanno l'ulteriore vantaggio di essere ereditabili. È ora possibile creare una sottoclasse di Fooe il metodo factory ereditato produrrebbe quindi istanze di quella sottoclasse.

Con una scelta tra una funzione e un metodo di classe, sceglierei il metodo di classe. Documenta chiaramente il tipo di oggetto che la fabbrica produrrà, senza doverlo esplicitare nel nome della funzione. Questo diventa molto più chiaro quando non hai solo più metodi di fabbrica, ma anche più classi.

Confronta le seguenti due alternative:

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()

Ancora meglio, l'utente finale potrebbe importare solo la Fooclasse e usarla per i vari modi in cui è possibile crearla, poiché tutti i metodi di fabbrica sono in un unico posto:

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

Il datetimemodulo stdlib utilizza ampiamente le fabbriche dei metodi di classe e la sua API è molto più chiara.


Bella risposta. Per ulteriori informazioni @classmethod, vedere Significato di @classmethod e @staticmethod per principianti?
nealmcb,

Grande attenzione alla funzionalità dell'utente finale
drtf

1

In Python, di solito preferisco una gerarchia di classi rispetto ai metodi di fabbrica. Qualcosa di simile a:

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)

Metterò l'intera gerarchia (e solo questa) in un modulo, diciamo foos.py. La classe base di Foosolito implementa tutti i metodi (e come tale, l'interfaccia), e di solito non è costruita direttamente dagli utenti. Le sottoclassi sono un mezzo per sovrascrivere il costruttore di base e specificare come Foodeve essere costruito. Quindi creerei un Foochiamando il costruttore appropriato, come:

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

Lo trovo più elegante e flessibile rispetto alle fabbriche costruite con metodi statici, metodi di classe o funzioni a livello di modulo.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.