Verwenden von Pythons Method Resolution Order für die Abhängigkeitsinjektion - ist das schlecht?

11

Ich habe gesehen, wie Raymond Hettingers Pycon "Super Considered Super" sprach, und ein wenig über Pythons MRO (Method Resolution Order) gelernt, das eine Klasse "Eltern" -Klassen auf deterministische Weise linearisiert. Wir können dies zu unserem Vorteil nutzen, wie im folgenden Code, um die Abhängigkeitsinjektion durchzuführen. Also jetzt möchte ich natürlich superfür alles verwenden!

Im folgenden Beispiel Userdeklariert die Klasse ihre Abhängigkeiten, indem sie von LoggingServiceund erbt UserService. Das ist nicht besonders speziell. Der interessante Teil ist, dass wir mit der Method Resolution Order auch Abhängigkeiten während des Unit-Tests verspotten können. Der folgende Code erstellt ein, MockUserServicedas UserServicevon den Methoden erbt und diese implementiert, die wir verspotten möchten. Im folgenden Beispiel stellen wir eine Implementierung von bereit validate_credentials. Um MockUserServiceAnrufe zu bearbeiten, müssen validate_credentialswir sie vorher UserServiceim MRO positionieren . Dies geschieht, indem eine Wrapper-Klasse um Useraufgerufen erstellt MockUserund von Userund geerbt wird MockUserService.

Wenn wir dies jetzt tun MockUser.authenticateund es wiederum Aufrufe an super().validate_credentials() MockUserServiceist , ist dies UserServicein der Reihenfolge der Methodenauflösung vorher und wird, da es eine konkrete Implementierung validate_credentialsdieser Implementierung bietet , verwendet. Ja - wir haben uns UserServicein unseren Unit-Tests erfolgreich verspottet . Bedenken Sie, dass UserServicedies einige teure Netzwerk- oder Datenbankaufrufe verursachen könnte - wir haben gerade den Latenzfaktor entfernt. Es besteht auch kein Risiko UserService, Live- / Produktdaten zu berühren.

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

Das fühlt sich ziemlich klug an, aber ist dies eine gute und gültige Verwendung von Pythons Mehrfachvererbung und Methodenauflösungsreihenfolge? Wenn ich über Vererbung nachdenke, wie ich OOP mit Java gelernt habe, fühlt sich das völlig falsch an, weil wir nicht sagen können, ob Useres ein UserServiceoder ein Userist LoggingService. So zu denken und die Vererbung so zu verwenden, wie es der obige Code verwendet, macht wenig Sinn. Oder ist es? Wenn wir die Vererbung nur verwenden, um die Wiederverwendung von Code bereitzustellen, und nicht in Eltern-Kind-Beziehungen denken, dann scheint dies nicht so schlimm zu sein.

Mache ich es falsch

Iain
quelle
Anscheinend gibt es hier zwei verschiedene Fragen: "Ist diese Art der MRO-Manipulation sicher / stabil?" und "Ist es ungenau zu sagen, dass Python-Vererbung eine" ist-eine "Beziehung modelliert?" Versuchen Sie, beide oder nur einen von ihnen zu fragen? (Sie sind beide gute Fragen, wollen nur sicherstellen, dass wir die richtige beantworten, oder diese in zwei Fragen aufteilen, wenn Sie nicht beide wollen)
Ixrec
Ich habe die Fragen beim Lesen angesprochen. Habe ich etwas ausgelassen?
Aaron Hall
@lxrec Ich denke du hast absolut recht. Ich versuche zwei verschiedene Fragen zu stellen. Ich denke, der Grund, warum sich dies nicht "richtig" anfühlt, ist, dass ich über "ist" einen Vererbungsstil nachdenke (also ist GoldenRetriever "ein" Hund und Hund "ist ein" Tier ") anstelle dieser Art von kompositorischer Ansatz. Ich denke, das ist etwas, für das ich eine andere Frage öffnen könnte :)
Iain
Das verwirrt mich auch erheblich. Wenn die Komposition der Vererbung vorzuziehen ist, warum nicht Instanzen von LoggingService und UserService an den Konstruktor von User übergeben und als Mitglieder festlegen? Dann könnten Sie die Ententypisierung für die Abhängigkeitsinjektion verwenden und stattdessen eine Instanz von MockUserService an den Benutzerkonstruktor übergeben. Warum ist die Verwendung von Super für DI vorzuziehen?
Jake Spracher

Antworten:

7

Verwenden von Pythons Method Resolution Order für die Abhängigkeitsinjektion - ist das schlecht?

Nein. Dies ist eine theoretisch beabsichtigte Verwendung des C3-Linearisierungsalgorithmus. Dies widerspricht Ihren vertrauten Beziehungen, aber einige betrachten die Komposition als der Vererbung vorzuziehen. In diesem Fall haben Sie einige Has-A-Beziehungen erstellt. Es scheint, dass Sie auf dem richtigen Weg sind (obwohl Python über ein Protokollierungsmodul verfügt, ist die Semantik etwas fragwürdig, aber als akademische Übung ist es vollkommen in Ordnung).

Ich denke nicht, dass Spott oder Affen-Patching eine schlechte Sache ist, aber wenn Sie sie mit dieser Methode vermeiden können, ist das gut für Sie - mit zugegebenermaßen mehr Komplexität haben Sie es vermieden, die Definitionen der Produktionsklassen zu ändern.

Mache ich es falsch

Es sieht gut aus. Sie haben eine möglicherweise teure Methode außer Kraft gesetzt, ohne Affen zu patchen oder einen Schein-Patch zu verwenden, was wiederum bedeutet, dass Sie die Definitionen der Produktionsklassen nicht einmal direkt geändert haben.

Wenn Sie die Funktionalität ausüben möchten, ohne tatsächlich Anmeldeinformationen im Test zu haben, sollten Sie wahrscheinlich Folgendes tun:

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

anstatt Ihre echten Anmeldeinformationen zu verwenden und zu überprüfen, ob die Parameter korrekt empfangen wurden, möglicherweise mit Zusicherungen (da dies schließlich Testcode ist):

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

Ansonsten sieht es so aus, als hätten Sie es herausgefunden. Sie können den MRO folgendermaßen überprüfen:

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

Und Sie können überprüfen, ob das MockUserServiceVorrang vor dem hat UserService.

Aaron Hall
quelle