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 super
para todo!
En el siguiente ejemplo, la User
clase declara sus dependencias heredando de ambos LoggingService
y 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 MockUserService
que hereda UserService
y proporciona una implementación de los métodos que queremos burlarnos. En el siguiente ejemplo, proporcionamos una implementación de validate_credentials
. Para poder MockUserService
manejar cualquier llamada, validate_credentials
debemos ubicarla antes UserService
en el MRO. Esto se hace creando una clase de contenedor alrededor de User
llamada MockUser
y haciendo que herede de User
y MockUserService
.
Ahora, cuando lo hacemos MockUser.authenticate
y, a su vez, llama a super().validate_credentials()
MockUserService
está antes UserService
en la Orden de resolución de método y, dado que ofrece una implementación concreta de validate_credentials
esta implementación, se utilizará. Sí, nos hemos burlado con éxito UserService
en nuestras pruebas unitarias. Tenga en cuenta que UserService
podrí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 UserService
tocar 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 User
es un UserService
o User
es 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?