Inizializzazione di classi composite in Python

2

Sto scrivendo un sistema Python in cui ho una classe principale ( PlayerModel ) che ha due altre classi come membri ( StateModel e ActionModel ). Mi stavo chiedendo, qual è il modo corretto per inizializzare tale classe composita?

Al momento. Sto creando separatamente le istanze dei membri e il metodo __init__ della classe assomiglia a questo:

def __init__(self, state_model, action_model):
    self.state_model = state_model
    self.action_model = action_model

Tuttavia, ciò significa che i tipi dei membri non sono noti alla classe in fase di compilazione (e perdo, ad esempio, il completamento automatico, che è piuttosto fastidioso).

Naturalmente posso istanziare i membri all'interno della classe principale, ma ciò significa che dovrei sempre passare i loro argomenti al costruttore della classe principale, trasformandolo in un metodo con 5-6 parametri, e non mi piacciono quelli (e nemmeno lo zio Bob).

Esiste una soluzione più elegante a questo problema rispetto all'aggregazione dei parametri delle classi membro in una tupla denominata?

    
posta Venomouse 19.02.2016 - 15:38
fonte

1 risposta

2

Prima di tutto, sii consapevole che Python è un linguaggio dinamico. Che "i tipi di membri non sono noti alla classe in fase di compilazione" è praticamente vero qualunque soluzione tu usi, perché Python non ha un sistema di tipo statico che assegna i tipi alle variabili o ai membri della classe. La buona autocompletazione è preziosa, ma non devi scommettere il tuo design sulle limitazioni del tuo editor.

Ci sono alcuni importanti compromessi quando si istanziano i campi all'interno o all'esterno della classe.

  • se l'istanziazione all'esterno, chi crea un'istanza della nostra classe ha bisogno di conoscere i dettagli interni della nostra classe per creare le due dipendenze. Questa è essenzialmente una violazione di Law of Demeter . D'altra parte, queste dipendenze potrebbero svolgere un ruolo significativo al di fuori della nostra classe.

  • se l'istanziazione all'esterno, non possiamo essere sicuri che la nostra classe detenga l'unico riferimento all'oggetto interno. Questo può portare a bug di altri codici che modificano lo stato dei nostri oggetti interni. Altri oggetti possono essere condivisi in modo sicuro, o anche dovrebbero essere condivisi (ad esempio una connessione esterna).

  • in caso di istanziazione all'interno, non possiamo facilmente fornire un oggetto fittizio per testare la nostra classe.

Per me, la domanda decisiva è se gli oggetti interni sono dipendenze che potrebbero essere selezionati in fase di runtime, o sono così grandi da dover essere testati separatamente. Nel tuo caso, la risposta a questi è probabilmente "sì". È molto più ragionevole istanziare queste classi all'esterno

player_model = PlayerModel(
    state_model=StateModel(...),
    action_model=ActionModel(...))

che aggiungere l'istanziazione degli altri modelli a tutte le responsabilità che già possiede. In particolare, la strategia di creare un'istanza di tutto nel costruttore porta a costruttori più grandi e gonfiati, più si cerca la catena di utilizzo. Non si tratta solo di contare i parametri, si tratta di separazione delle preoccupazioni .

Se non è ragionevole che il chiamante si occupi di creare un'istanza di tutte le dipendenze, a volte creo una factory per fare questo:

class Dependencies(object):
  def __init__(self, global_configuration):
    ...

  def new_player_model(self, ...):
    return PlayerModel(
      state_model=self.new_state_model(...),
      action_model=self.new_action_model(...))

  def new_state_model(self, ...): return StateModel(...)
  def new_action_model(self, ...): return ActionModel(...)

Quindi:

deps = Dependencies(...)
player_model = deps.new_player_model(...)

Spesso, molti degli argomenti del costruttore sono solo varie dipendenze che devono essere soddisfatte. I nostri metodi factory nell'oggetto Dependencies devono solo inoltrare gli altri argomenti, poiché le dipendenze possono essere gestite internamente. In tal caso, la gestione centralizzata delle dipendenze può essere molto utile.

Se un gran numero di argomenti è inevitabile, ci sono un paio di strategie per far fronte a questo. Il più importante è integrato con Python: named parameters . Quando dichiari una funzione def foo(a, b, c): ... , puoi invocarla come foo("a", c="c", b="b") . Si noti che queste etichette sono opzionali e l'ordine è arbitrario e che Python genererà un errore se tutti gli argomenti richiesti sono stati dimenticati. Questo rende i nomi delle variabili dei tuoi parametri importanti perché possono essere usati per i parametri con nome. Con i parametri denominati, è ragionevole gestire un numero elevato di argomenti. Qui, il consiglio su molti parametri di "Uncle Bob" e altri si applica solo con limitazioni, dal momento che il loro consiglio è rivolto principalmente a linguaggi come Java che mancano di parametri con nome.

Tuttavia, i parametri con nome non risolvono tutti i problemi ( matplotlib.pyplot è il mio controesempio preferito). Spesso, ci saranno gruppi naturali di parametri. Alcuni parametri possono essere usati solo insieme (ad esempio, una x e y coordinate). Possiamo quindi introdurre piccoli oggetti che contengono questi argomenti e non fanno nient'altro. Applicato al tuo caso, potremmo avere StateModelArgs e ActionModelArgs o qualche altro raggruppamento. Il chiamante fornisce quindi questi argomenti, ma gli oggetti reali vengono istanziati da questi argomenti all'interno della nostra classe. Queste strategie hanno alcuni vantaggi, ad es. questa convalida può essere eseguita più facilmente negli oggetti argomento, ma potrebbe anche fornire un valore così basso che sarebbe più semplice creare un'istanza diretta degli oggetti reali.

Quindi quale strategia utilizzare dipende molto dalle circostanze specifiche, e entrambe le scelte possono avere senso.

    
risposta data 19.02.2016 - 16:42
fonte

Leggi altre domande sui tag