Korzystanie z metody rozpoznawania kolejności w Pythonie dla wstrzykiwania zależności - czy to źle?

11

Widziałem, jak Raymond Hettinger Pycon mówi „Super uważany za super” i dowiedziałem się trochę o MRO Pythona (Order Resolution Order), który deterministycznie interpretuje klasy „nadrzędne”. Możemy to wykorzystać na naszą korzyść, tak jak w poniższym kodzie, aby wykonać wstrzyknięcie zależności. Więc teraz oczywiście chcę używać superdo wszystkiego!

W poniższym przykładzie Userklasa deklaruje swoje zależności, dziedzicząc zarówno z, jak LoggingServicei UserService. To nie jest specjalnie wyjątkowe. Interesującą częścią jest to, że możemy użyć metody Resolution Resolution Order, aby wykpić zależności podczas testów jednostkowych. Poniższy kod tworzy MockUserServicedziedziczenie UserServicei zapewnia implementację metod, które chcemy wyśmiewać. W poniższym przykładzie zapewniamy implementację validate_credentials. Aby MockUserServiceobsłużyć wszelkie połączenia validate_credentials, musimy wcześniej ustawić go UserServicew MRO. Odbywa się to poprzez utworzenie klasy otoki wokół i Userwywołanie MockUserjej dziedziczenia z Useri MockUserService.

Teraz, kiedy mamy zrobić MockUser.authenticatei go z kolei domaga się super().validate_credentials() MockUserServiceto zanim UserServicew Metodzie Uchwałą Zakonu i, ponieważ oferuje implementacja beton z validate_credentialstej implementacji zostaną wykorzystane. Tak - z powodzeniem wyśmiewaliśmy się UserServicew naszych testach jednostkowych. Pomyśl, że UserServicemoże to powodować kosztowne połączenia sieciowe lub bazy danych - właśnie usunęliśmy czynnik opóźniający. Nie ma również ryzyka UserServicedotknięcia danych na żywo / 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())

To wydaje się dość sprytne, ale czy jest to dobre i prawidłowe wykorzystanie wielokrotnego dziedziczenia Pythona i kolejności rozwiązywania metod? Kiedy myślę o dziedziczeniu w sposobie, w jaki nauczyłem się OOP z Javą, wydaje się to całkowicie błędne, ponieważ nie możemy powiedzieć, że Userjest a UserServicelub Userjest LoggingService. Myślenie w ten sposób, używanie dziedziczenia tak, jak wykorzystuje powyższy kod, nie ma większego sensu. Albo to jest? Jeśli używamy dziedziczenia wyłącznie w celu zapewnienia ponownego użycia kodu, a nie myślenia w kategoriach relacji rodzic-> dzieci, nie wydaje się to takie złe.

Czy robię to źle?

Iain
źródło
Wygląda na to, że są tutaj dwa różne pytania: „Czy tego rodzaju manipulowanie MRO jest bezpieczne / stabilne?” oraz „Czy niedopuszczalne jest stwierdzenie, że dziedziczenie w Pythonie modeluje relację„ jest-a ”? Próbujesz zapytać o jedno z nich, czy tylko o jedno? (oba są dobrymi pytaniami, tylko upewnij się, że odpowiemy na właściwe, lub podziel je na dwa pytania, jeśli nie chcesz obu)
Ixrec
Czytałem pytania, czy coś pominąłem?
Aaron Hall
@lxrec Myślę, że masz absolutną rację. Próbuję zadać dwa różne pytania. Wydaje mi się, że powodem, dla którego nie wydaje się to „właściwe”, jest to, że myślę o „stylu odziedziczonym” (więc GoldenRetriever „jest„ psem i psem ”jest„ zwierzęciem ”zamiast tego rodzaju podejście kompozycyjne. Myślę, że jest to coś, na co mogę otworzyć kolejne pytanie :)
Iain
To także znacznie mnie dezorientuje. Jeśli kompozycja jest lepsza niż dziedziczenie, dlaczego nie przekazać instancji LoggingService i UserService do konstruktora użytkownika i ustawić je jako członków? Następnie można użyć wpisywania kaczki do wstrzykiwania zależności i przekazać zamiast tego instancję MockUserService do konstruktora użytkownika. Dlaczego używanie super do DI jest lepsze?
Jake Spracher,

Odpowiedzi:

7

Korzystanie z metody rozpoznawania kolejności w Pythonie dla wstrzykiwania zależności - czy to źle?

Nie. Jest to teoretycznie zamierzone zastosowanie algorytmu linearyzacji C3. Jest to sprzeczne z twoimi znanymi relacjami, ale niektórzy uważają kompozycję za preferowaną niż dziedziczenie. W tym przypadku skomponowałeś niektóre relacje typu „has-a”. Wygląda na to, że jesteś na dobrej drodze (chociaż Python ma moduł rejestrowania, więc semantyka jest nieco wątpliwa, ale jako ćwiczenie akademickie jest całkowicie w porządku).

Nie sądzę, by kpina lub łatanie małp było złą rzeczą, ale jeśli możesz ich uniknąć za pomocą tej metody, to dobrze dla Ciebie - przy znacznie większej złożoności uniknąłeś modyfikacji definicji klas produkcyjnych.

Czy robię to źle?

Wygląda dobrze. Przesłoniłeś potencjalnie kosztowną metodę, bez łatania małp lub używania fałszywej łatki, co znowu oznacza, że ​​nawet nie zmodyfikowałeś bezpośrednio definicji klas produkcyjnych.

Jeśli celem było skorzystanie z funkcjonalności bez posiadania poświadczeń w teście, prawdopodobnie powinieneś zrobić coś takiego:

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

zamiast używać prawdziwych poświadczeń i sprawdzać, czy parametry są poprawnie odbierane, być może z zapewnieniami (bo przecież jest to kod testowy.):

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

W przeciwnym razie wygląda na to, że to rozgryzłeś. Możesz zweryfikować MRO w następujący sposób:

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

I możesz sprawdzić, czy MockUserServicema on pierwszeństwo przed UserService.

Aaron Hall
źródło