Utilisation de l'ordre de résolution de méthode de Python pour l'injection de dépendances - est-ce mauvais?

11

J'ai regardé Pycon de Raymond Hettinger parler de "Super Considéré Super" et j'ai appris un peu sur le MRO de Python (Ordre de résolution de méthode) qui linéarise les classes "parent" d'une manière déterministe. Nous pouvons l'utiliser à notre avantage, comme dans le code ci-dessous, pour effectuer une injection de dépendance. Alors maintenant, naturellement, je veux l'utiliser superpour tout!

Dans l'exemple ci-dessous, la Userclasse déclare ses dépendances en héritant des deux LoggingServiceet UserService. Ce n'est pas particulièrement spécial. La partie intéressante est que nous pouvons utiliser l'Ordre de résolution de méthode pour simuler les dépendances lors des tests unitaires. Le code ci-dessous crée un MockUserServicequi hérite de UserServiceet fournit une implémentation des méthodes que nous voulons simuler. Dans l'exemple ci-dessous, nous fournissons une implémentation de validate_credentials. Afin de MockUserServicegérer tous les appels à, validate_credentialsnous devons le positionner auparavant UserServicedans le MRO. Cela se fait en créant une classe wrapper autour de Userappelée MockUseret en la faisant hériter de Useret MockUserService.

Maintenant, quand nous le faisons MockUser.authenticateet, à son tour, les appels à super().validate_credentials() MockUserServiceest avant UserServicedans l'ordre de résolution de méthode et, puisqu'il offre une implémentation concrète de validate_credentialscette implémentation, ils seront utilisés. Oui - nous avons réussi à nous moquer UserServicede nos tests unitaires. Considérez que cela UserServicepourrait faire des appels réseau ou de base de données coûteux - nous venons de supprimer le facteur de latence de cela. Il n'y a pas non plus de risque de UserServicetoucher aux données live / 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())

Cela semble assez intelligent, mais est-ce une bonne et valide utilisation de l'héritage multiple et de l'ordre de résolution de méthode de Python? Quand je pense à l'héritage dans la façon dont j'ai appris la POO avec Java, cela semble complètement faux parce que nous ne pouvons pas dire Userest UserServiceou Userest un LoggingService. Penser de cette façon, utiliser l'héritage de la façon dont le code ci-dessus l'utilise n'a pas beaucoup de sens. Ou est-ce? Si nous utilisons l'héritage uniquement pour fournir une réutilisation du code, et sans penser en termes de relations parents-> enfants, cela ne semble pas si mal.

Suis-je en train de mal faire?

Iain
la source
Il semble qu'il y ait deux questions différentes ici: "Ce type de manipulation MRO est-il sûr / stable?" et "Est-il inexact de dire que l'héritage Python modélise une relation" est-une "?" Essayez-vous de poser les deux, ou juste l'un d'eux? (ce sont deux bonnes questions, je veux juste m'assurer que nous répondons à la bonne, ou diviser cela en deux questions si vous ne voulez pas les deux)
Ixrec
J'ai répondu aux questions en le lisant, ai-je oublié quelque chose?
Aaron Hall
@lxrec Je pense que vous avez absolument raison. J'essaie de poser deux questions différentes. Je pense que la raison pour laquelle cela ne semble pas "correct" est parce que je pense à "est-un" style d'héritage (donc GoldenRetriever "est-un" Chien et Chien "est-un" Animal) au lieu de ce type de approche compositionnelle. Je pense que c'est quelque chose pour
Iain
Cela me perturbe également beaucoup. Si la composition est préférable à l'héritage, pourquoi ne pas transmettre des instances de LoggingService et UserService au constructeur de User et les définir comme membres? Ensuite, vous pouvez utiliser le typage canard pour l'injection de dépendances et transmettre à la place une instance de MockUserService au constructeur User. Pourquoi utiliser super pour DI est-il préférable?
Jake Spracher

Réponses:

7

Utilisation de l'ordre de résolution de méthode de Python pour l'injection de dépendances - est-ce mauvais?

Non. Il s'agit d'une utilisation théorique prévue de l'algorithme de linéarisation C3. Cela va à l'encontre de vos relations familières, mais certains considèrent la composition comme préférable à l'héritage. Dans ce cas, vous avez composé des relations has-a. Il semble que vous soyez sur la bonne voie (bien que Python ait un module de journalisation, la sémantique est donc un peu discutable, mais en tant qu'exercice académique, c'est très bien).

Je ne pense pas que la moquerie ou le patch de singe soit une mauvaise chose, mais si vous pouvez les éviter avec cette méthode, c'est bon pour vous - avec certes plus de complexité, vous avez évité de modifier les définitions de classe de production.

Suis-je en train de mal faire?

Ça à l'air bon. Vous avez remplacé une méthode potentiellement coûteuse, sans patch de singe ni utilisation d'un patch factice, ce qui, encore une fois, signifie que vous n'avez même pas directement modifié les définitions de classe de production.

Si l'intention était d'exercer la fonctionnalité sans avoir réellement d'informations d'identification dans le test, vous devriez probablement faire quelque chose comme:

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

au lieu d'utiliser vos véritables informations d'identification, et vérifiez que les paramètres sont reçus correctement, peut-être avec des assertions (car c'est du code de test, après tout.):

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

Sinon, on dirait que vous l'avez compris. Vous pouvez vérifier le MRO comme ceci:

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

Et vous pouvez vérifier que le MockUserServicea priorité sur le UserService.

Aaron Hall
la source