Uso del método de resolución de Python Orden para la inyección de dependencias: ¿es malo?

11

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?

Iain
fuente
Parece que hay dos preguntas diferentes aquí: "¿Es este tipo de manipulación de MRO seguro / estable?" y "¿Es incorrecto decir que la herencia de Python modela una relación" es-una "? ¿Estás tratando de preguntar a ambos, o solo a uno de ellos? (ambas son buenas preguntas, solo quiero asegurarnos de que respondamos la correcta, o dividir esto en dos preguntas si no quieres las dos)
Ixrec
He respondido las preguntas mientras lo leía, ¿he dejado algo fuera?
Aaron Hall el
@lxrec Creo que tienes toda la razón. Estoy tratando de hacer dos preguntas diferentes. Creo que la razón por la que esto no se siente "correcto" es porque estoy pensando en el estilo de herencia "es-un" (entonces GoldenRetriever "es-un" Perro y Perro "es-un" Animal) en lugar de este tipo de enfoque compositivo. Creo que esto es algo para lo que podría abrir otra pregunta :)
Iain
Esto también me confunde significativamente. Si la composición es preferible a la herencia, ¿por qué no pasar instancias de LoggingService y UserService al constructor de User y establecerlas como miembros? Luego, podría usar la escritura de pato para la inyección de dependencia y pasar una instancia de MockUserService al constructor de Usuario en su lugar. ¿Por qué es preferible usar super para DI?
Jake Spracher

Respuestas:

7

Uso del método de resolución de Python Orden para la inyección de dependencias: ¿es malo?

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.

¿Lo estoy haciendo mal?

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:

>>> print(MockUser('foo', 'bar').authenticate())
> MockUserService::validate_credentials
True

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):

def validate_credentials(self, username, password):
    print('> MockUserService::validate_credentials')
    assert username_ok(username), 'username expected to be ok'
    assert password_ok(password), 'password expected to be ok'
    return True

De lo contrario, parece que lo has descubierto. Puede verificar el MRO de esta manera:

>>> MockUser.mro()
[<class '__main__.MockUser'>, 
 <class '__main__.User'>, 
 <class '__main__.LoggingService'>, 
 <class '__main__.MockUserService'>, 
 <class '__main__.UserService'>, 
 <class 'object'>]

Y puede verificar que MockUserServicetenga prioridad sobre UserService.

Aaron Hall
fuente