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?
Respuestas:
No. Este es un uso teórico previsto del algoritmo de linealización C3. Esto va en contra de sus relaciones familiares, pero algunos consideran que la composición es preferible a la herencia. En este caso, compusiste algunas relaciones has-a. Parece que estás en el camino correcto (aunque Python tiene un módulo de registro, por lo que la semántica es un poco cuestionable, pero como ejercicio académico está perfectamente bien).
No creo que burlarse o parchear a los monos sea algo malo, pero si puede evitarlos con este método, es bueno para usted; sin duda, con más complejidad, ha evitado modificar las definiciones de la clase de producción.
Se ve bien. Ha anulado un método potencialmente costoso, sin parches de mono o utilizando un parche simulado, lo que, nuevamente, significa que ni siquiera ha modificado directamente las definiciones de la clase de producción.
Si la intención era ejercer la funcionalidad sin tener credenciales en la prueba, probablemente debería hacer algo como:
en lugar de usar sus credenciales reales, y verifique que los parámetros se reciban correctamente, tal vez con aserciones (ya que este es el código de prueba, después de todo):
De lo contrario, parece que lo has descubierto. Puede verificar el MRO de esta manera:
Y puede verificar que
MockUserService
tenga prioridad sobreUserService
.fuente