In Python, ereditare da classi mixin-like è un approccio ok-ish poiché il linguaggio supporta l'ereditarietà multipla. L'unica avvertenza è il metodo __init__()
: tutte le classi da cui si eredita devono avere costruttori compatibili poiché il loro ordine non è noto al momento della definizione della classe.
In pratica, l'utilizzo dell'ereditarietà di solito non è appropriato in quanto la delega può ottenere lo stesso risultato con un mal di testa molto inferiore. Quindi invece di
class Mixin(object):
def __init__(self, x):
self.__x = x
def method(self):
...
class A(Mixin):
def __init__(self, x):
super().__init__(x)
...
class B(Mixin):
def __init__(self, x):
super().__init__(x)
...
Potremmo dire
class Mixin(object):
def __init__(self, x):
self.__x = x
def method(self):
...
class A(Mixin):
def __init__(self, x):
self.__mixin = Mixin(x)
def method(self):
return self.__mixin.method()
...
class B(Mixin):
def __init__(self, x):
self.__mixin = Mixin(x)
def method(self):
return self.__mixin.method()
...
(Lo svantaggio della delega manuale è che le docstring si perdono, ma prevenire in modo affidabile è un po 'non banale a causa delle più sottili complessità del protocollo di risoluzione dei membri dell'oggetto Python.)
Se il mixin non ha né più metodi pubblici né dati associati, è preferibile una funzione libera invece di una classe di mixin:
def common_method(self):
...
class A(Mixin):
def method(self):
return common_method()
...
class B(Mixin):
def method(self):
return common_method()
...
La delega è inappropriata solo nel caso in cui sia necessario eseguire controlli isinstance(o, Mixin)
. Nella maggior parte dei casi questo è un difetto di progettazione e può essere evitato, specialmente se il codice che esegue tale controllo è sotto il tuo controllo. In caso contrario, il modello dell'adattatore dell'oggetto potrebbe essere adatto, che è fondamentalmente uguale all'esempio di delega, ma capovolto:
class RequiredType(object):
...
class ActualType(object):
...
class Adapter(RequiredType):
def __init__(self, target):
self.__target = target
def method_of_required_type(self):
return self.__target.method_of_actual_type()
Ora data una funzione needs_required_type(...)
, data un'istanza del tipo effettivo potresti chiamarla needs_required_type(Adapter(instance))
. Di solito è preferibile ereditare il tipo richiesto nel tipo effettivo, dal momento che l'ereditarietà espone un sacco di metodi e campi indesiderati. Ciò è particolarmente negativo per tipi predefiniti come str
o list
. Ho visto casi che hanno reso un'API assolutamente incomprensibile perché una classe ha ereditato da list
come un dettaglio di implementazione, il che ha praticamente rovinato ogni pretesa di incapsulamento.