Ho visto il discorso di Raymond Hettinger su Pycon "Super Considerato Super" e ho imparato un po 'sull'MRO di Python (Method Resolution Order) che linearizza le classi "parent" delle classi in modo deterministico. Possiamo usare questo a nostro vantaggio, come nel codice sottostante, per fare un'iniezione di dipendenza. Quindi ora, naturalmente, voglio usare super
per tutto!
Nell'esempio seguente, la classe User
dichiara le sue dipendenze ereditando da LoggingService
e UserService
. Questo non è particolarmente speciale. La parte interessante è che possiamo usare l'ordine di risoluzione dei metodi anche per deridere le dipendenze durante i test unitari. Il codice seguente crea un MockUserService
che eredita da UserService
e fornisce un'implementazione dei metodi che vogliamo prendere in giro. Nell'esempio seguente, forniamo un'implementazione di validate_credentials
. Per fare in modo che% co_de gestisca le chiamate su MockUserService
, dobbiamo posizionarlo prima di validate_credentials
nell'MRO. Ciò avviene creando una classe wrapper attorno a UserService
chiamata User
e ereditandola da MockUser
e User
.
Ora, quando facciamo MockUserService
e, a sua volta, le chiamate a MockUser.authenticate
super().validate_credentials()
è prima di MockUserService
nell'ordine di risoluzione dei metodi e, poiché offre un'implementazione concreta di UserService
, questa implementazione essere usato. Sì, abbiamo superato con successo validate_credentials
nei nostri test unitari. Considera che UserService
potrebbe effettuare alcune costose chiamate di rete o di database: abbiamo appena rimosso il fattore di latenza. Inoltre, non vi è alcun rischio che UserService
tocchi dati live / prod.
class LoggingService(object):
"""
Just a contrived logging class for demonstration purposes
"""
def log_error(self, error):
pass
class UserService(object):
"""
Provide a method to authenticate the user by performing some expensive DB or network operation.
"""
def validate_credentials(self, username, password):
print('> UserService::validate_credentials')
return username == 'iainjames88' and password == 'secret'
class User(LoggingService, UserService):
"""
A User model class for demonstration purposes. In production, this code authenticates user credentials by calling
super().validate_credentials and having the MRO resolve which class should handle this call.
"""
def __init__(self, username, password):
self.username = username
self.password = password
def authenticate(self):
if super().validate_credentials(self.username, self.password):
return True
super().log_error('Incorrect username/password combination')
return False
class MockUserService(UserService):
"""
Provide an implementation for validate_credentials() method. Now, calls from super() stop here when part of MRO.
"""
def validate_credentials(self, username, password):
print('> MockUserService::validate_credentials')
return True
class MockUser(User, MockUserService):
"""
A wrapper class around User to change it's MRO so that MockUserService is injected before UserService.
"""
pass
if __name__ == '__main__':
# Normal useage of the User class which uses UserService to resolve super().validate_credentials() calls.
user = User('iainjames88', 'secret')
print(user.authenticate())
# Use the wrapper class MockUser which positions the MockUserService before UserService in the MRO. Since the class
# MockUserService provides an implementation for validate_credentials() calls to super().validate_credentials() from
# MockUser class will be resolved by MockUserService and not passed to the next in line.
mock_user = MockUser('iainjames88', 'secret')
print(mock_user.authenticate())
Questo sembra abbastanza intelligente, ma è un uso valido e valido dell'eredità multipla di Python e dell'ordine di risoluzione dei metodi? Quando penso all'ereditarietà nel modo in cui ho imparato l'OOP con Java, mi sento completamente sbagliato perché non possiamo dire che UserService
è un User
o UserService
è un User
. Pensando in questo modo, l'uso dell'ereditarietà come il codice precedente lo usa non ha molto senso. O è? Se usiamo l'ereditarietà solo per fornire il riutilizzo del codice, e non pensando in termini di relazioni genitore-genitore, allora questo non sembra così male.
Sto sbagliando?