Ho visto Pycon di Raymond Hettinger parlare di "Super Considered Super" e ho imparato un po 'dell'MRO (Method Resolution Order) di Python che linearizza le classi "parent" in modo deterministico. Possiamo usarlo a nostro vantaggio, come nel codice seguente, per fare l'iniezione di dipendenza. Quindi ora, naturalmente, voglio usare super
per tutto!
Nell'esempio seguente, la User
classe dichiara le sue dipendenze ereditando da entrambi LoggingService
e UserService
. Questo non è particolarmente speciale. La parte interessante è che possiamo usare l'Ordine di risoluzione del metodo anche deridere le dipendenze durante i test delle unità. Il codice seguente crea un MockUserService
elemento che eredita UserService
e fornisce un'implementazione dei metodi che vogliamo deridere. Nell'esempio seguente, forniamo un'implementazione di validate_credentials
. Per poter MockUserService
gestire qualsiasi chiamata, validate_credentials
è necessario posizionarlo prima UserService
nell'MRO. Ciò è stato creato creando una classe wrapper attorno a User
richiamata MockUser
e facendola ereditare da User
e MockUserService
.
Ora, quando lo facciamo MockUser.authenticate
e, a sua volta, si chiama in super().validate_credentials()
MockUserService
precedenza UserService
nell'ordine di risoluzione del metodo e, poiché offre un'implementazione concreta di validate_credentials
questa implementazione, verrà utilizzata. Sì, abbiamo deriso con successo i UserService
nostri test unitari. Considera che UserService
potrebbe fare alcune costose chiamate di rete o di database: abbiamo appena rimosso il fattore di latenza di questo. Non vi è inoltre alcun rischio di UserService
toccare i 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())
Sembra abbastanza intelligente, ma è un buon e valido uso dell'eredità multipla di Python e dell'ordine di risoluzione dei metodi? Quando penso all'eredità nel modo in cui ho appreso OOP con Java, mi sembra completamente sbagliato perché non possiamo dire che User
sia UserService
o User
sia un LoggingService
. Pensare in questo modo, usare l'ereditarietà come usa il codice sopra riportato non ha molto senso. O è? Se usiamo l'eredità solo per fornire il riutilizzo del codice e non pensare in termini di relazioni genitori-> figli, allora questo non sembra così male.
Sto sbagliando?