Vi a Pycon de Raymond Hettinger hablar "Super Considerado Súper" y aprendí un poco sobre el MRO de Python (Orden de Resolución de Método) que linealiza las clases "padre" de una manera determinista. Podemos usar esto para nuestra ventaja, como en el siguiente código, para hacer una inyección de dependencia. ¡Así que ahora, naturalmente, quiero usarlo superpara todo!
En el siguiente ejemplo, la Userclase declara sus dependencias heredando de ambos LoggingServicey UserService. Esto no es particularmente especial. La parte interesante es que podemos usar la Orden de resolución de métodos también simula las dependencias durante las pruebas unitarias. El siguiente código crea uno MockUserServiceque hereda UserServicey proporciona una implementación de los métodos que queremos burlarnos. En el siguiente ejemplo, proporcionamos una implementación de validate_credentials. Para poder MockUserServicemanejar cualquier llamada, validate_credentialsdebemos ubicarla antes UserServiceen el MRO. Esto se hace creando una clase de contenedor alrededor de Userllamada MockUsery haciendo que herede de Usery MockUserService.
Ahora, cuando lo hacemos MockUser.authenticatey, a su vez, llama a super().validate_credentials() MockUserServiceestá antes UserServiceen la Orden de resolución de método y, dado que ofrece una implementación concreta de validate_credentialsesta implementación, se utilizará. Sí, nos hemos burlado con éxito UserServiceen nuestras pruebas unitarias. Tenga en cuenta que UserServicepodría hacer algunas llamadas costosas a la red o a la base de datos; acabamos de eliminar el factor de latencia de esto. Tampoco hay riesgo de UserServicetocar datos en 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())
Esto se siente bastante inteligente, pero ¿es este un uso bueno y válido de la herencia múltiple de Python y el Orden de resolución de métodos? Cuando pienso en la herencia en la forma en que aprendí OOP con Java, esto se siente completamente mal porque no podemos decir que Useres un UserServiceo Useres un LoggingService. Pensando de esa manera, usar la herencia de la manera en que lo usa el código anterior no tiene mucho sentido. ¿O es eso? Si usamos la herencia solo para proporcionar la reutilización del código, y no pensamos en términos de relaciones padre-> hijos, entonces esto no parece tan malo.
¿Lo estoy haciendo mal?