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?

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
MockUserServicetenga prioridad sobreUserService.fuente