Eu assisti o Pycon de Raymond Hettinger falar "Super Considerado Super" e aprendi um pouco sobre o MRO (Method Resolution Order) do Python, que lineariza as classes "parentais" de uma maneira determinística. Podemos usar isso para nossa vantagem, como no código abaixo, para fazer injeção de dependência. Então agora, naturalmente, quero usar super
para tudo!
No exemplo abaixo, a User
classe declara suas dependências herdando de ambos LoggingService
e UserService
. Isso não é particularmente especial. A parte interessante é que podemos usar a Ordem de Resolução de Método e simular dependências durante o teste de unidade. O código abaixo cria um MockUserService
que herda UserService
e fornece uma implementação dos métodos que queremos zombar. No exemplo abaixo, fornecemos uma implementação de validate_credentials
. Para podermos MockUserService
lidar com todas as chamadas validate_credentials
, precisamos posicioná-lo antes UserService
no MRO. Isso é feito criando uma classe de wrapper User
chamada MockUser
e herdando-a de User
e MockUserService
.
Agora, quando o fizermos MockUser.authenticate
, por sua vez, chama para super().validate_credentials()
MockUserService
está UserService
na Ordem de Resolução de Método e, uma vez que oferece uma implementação concreta validate_credentials
dessa implementação, será usado. Sim - nós zombamos com sucesso UserService
em nossos testes de unidade. Considere que isso UserService
pode fazer algumas chamadas caras de rede ou banco de dados - acabamos de remover o fator de latência disso. Também não há risco de UserService
tocar em dados ao vivo / 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())
Isso parece bastante inteligente, mas esse é um uso bom e válido da herança múltipla do Python e da Ordem de resolução de métodos? Quando penso em herança da maneira que aprendi OOP com Java, isso parece completamente errado, porque não podemos dizer que User
é um UserService
ou User
é um LoggingService
. Pensando dessa maneira, usar herança da maneira que o código acima usa, não faz muito sentido. Ou é? Se usarmos a herança apenas para fornecer reutilização de código, e não pensar em termos de relacionamento entre pais e filhos, isso não parecerá tão ruim.
Estou fazendo errado?