Corso Python / Scegliere tra ereditarietà e composizione

L’ereditarietà estende una relazione tra classi e il comportamento che ne deriva; la composizione fa usare a un oggetto un altro oggetto come componente. Scegli in base al contratto che deve restare valido e alla collaborazione che vuoi sostituire, non soltanto alla somiglianza di alcune righe di codice.

Valutare relazione e direzione delle modifiche

Una sottoclasse deve poter operare nei contesti previsti per la classe di base rispettandone il significato. Se restringe arbitrariamente gli ingressi ammessi o cambia il senso del risultato, riusare il nome del metodo non basta a renderla compatibile.

Un registro usa una politica di commissione, ma non è una particolare commissione: conservarla come componente descrive meglio questa relazione. Un oggetto FixedFee può invece essere un tipo di FeePolicy. I due meccanismi possono quindi convivere nello stesso programma.

La guida ufficiale all’ereditarietà spiega la definizione delle classi derivate. super() richiama l’implementazione successiva nell’ordine di risoluzione dei metodi; non significa sempre «la classe genitore scritta con un nome fisso». In questa lezione usiamo una gerarchia semplice, senza introdurre le ulteriori regole della cooperazione tra più classi base.

Prima di creare una gerarchia, chiarisci quale stato e comportamento appartengono all’oggetto e quale variazione deve poter essere sostituita.

Eseguire una politica di commissione sostituibile

Salva il codice completo come composition.py, in UTF-8, in una cartella di esercizio. Avvialo con python composition.py dal terminale con l’interprete del progetto selezionato. Fuori dall’ambiente virtuale su macOS o Ubuntu usa python3 se è il comando scelto. Non servono librerie esterne.

class FeePolicy:
    def apply(self, amount):
        return amount

class FixedFee(FeePolicy):
    def __init__(self, fee):
        self.fee = fee

    def apply(self, amount):
        return super().apply(amount) + self.fee

class Ledger:
    def __init__(self, policy):
        self.policy = policy

    def charge(self, amount):
        return self.policy.apply(amount)

print(Ledger(FeePolicy()).charge(8000))
print(Ledger(FixedFee(500)).charge(8000))

Il risultato previsto è:

8000
8500

FeePolicy restituisce l’importo senza modificarlo. FixedFee eredita il metodo apply e lo ridefinisce: la chiamata a super mantiene il comportamento di base, poi aggiunge fee. Ledger conserva il componente ricevuto e delega a esso il calcolo con policy.apply(amount).

La prima riga usa la politica base, la seconda una commissione fissa di 500. Il registro non contiene condizioni sul tipo di commissione e non deve essere riscritto per usare l’altra politica. Gli importi sono numeri nelle stesse unità; non viene effettuata alcuna conversione di valuta o validazione generale di un pagamento.

Confrontare due alternative per lo stesso requisito

Nel primo programma ereditarietà e composizione lavorano insieme. Per isolare la scelta, il programma seguente applica invece una commissione tramite un metodo ridefinito oppure tramite una funzione ricevuta dall’esterno. Salvalo come policies.py, senza sostituire composition.py, ed esegui python policies.py.

class InheritedLedger:
    def fee(self):
        return 0
    def charge(self, amount):
        return amount + self.fee()
class FixedLedger(InheritedLedger):
    def fee(self):
        return 500
class ComposedLedger:
    def __init__(self, fee_policy):
        self.fee_policy = fee_policy
    def charge(self, amount):
        return amount + self.fee_policy()
def fixed_fee():
    return 500
print(FixedLedger().charge(8000))
ledger = ComposedLedger(fixed_fee)
print(ledger.charge(8000))
ledger.fee_policy = lambda: 0
print(ledger.charge(8000))

Il risultato previsto è:

8500
8500
8000

FixedLedger cambia fee ereditando il resto del calcolo. ComposedLedger riceve un oggetto chiamabile senza argomenti che restituisce la commissione. I primi due risultati sono uguali; poi la sostituzione del solo componente fee_policy con una funzione che restituisce zero porta il risultato a 8000.

Le due interfacce nei programmi sono diverse: composition.py richiede un oggetto con apply(amount), mentre policies.py richiede una funzione chiamabile senza argomenti che produce solo la commissione. Non puoi scambiarle alla cieca. Un metodo con il nome giusto ma una firma incompatibile può fallire con TypeError, mentre un oggetto privo di apply può fallire con AttributeError.

Python non obbliga ogni collaboratore con apply a ereditare FeePolicy. Può bastare un oggetto con il comportamento concordato, ma firma, significato del risultato, errori e condizioni accettate fanno parte del contratto. Un piccolo componente di prova può aiutarti a verificare che Ledger deleghi effettivamente il calcolo senza dipendere da dettagli della politica.

Verificare il contratto prima di aggiungere complessità

  • La classe derivata mantiene gli ingressi e il significato dei risultati richiesti dalla base?
  • Stai ereditando molte operazioni estranee solo per riutilizzarne una?
  • Passare una funzione o un componente sostituibile renderebbe più esplicita la variazione?
  • La politica deve cambiare durante la vita dello stesso oggetto oppure rappresenta una relazione stabile di sottotipo?

Prova in composition.py una commissione fissa zero: entrambi i risultati devono essere 8000. Ripristina 500 e verifica 8000, 8500. In policies.py modifica solo la funzione fixed_fee per restituire zero: tutti e tre i risultati diventano 8000; ripristina poi 500 per tornare al confronto originale.

Le classi mostrate non rifiutano automaticamente importi o commissioni negativi. Se il dominio li vieta, devi aggiungere e verificare quelle regole nell’interfaccia appropriata. Mantieni input, stampa e salvataggio fuori dal calcolo della politica quando non fanno parte della sua responsabilità, così le alternative si confrontano sugli stessi dati. Per un problema piccolo può bastare una funzione: una gerarchia deve rendere più chiaro il modello, non soltanto aumentare il numero di classi.