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ć super
do wszystkiego!
W poniższym przykładzie User
klasa deklaruje swoje zależności, dziedzicząc zarówno z, jak LoggingService
i 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 MockUserService
dziedziczenie UserService
i zapewnia implementację metod, które chcemy wyśmiewać. W poniższym przykładzie zapewniamy implementację validate_credentials
. Aby MockUserService
obsłużyć wszelkie połączenia validate_credentials
, musimy wcześniej ustawić go UserService
w MRO. Odbywa się to poprzez utworzenie klasy otoki wokół i User
wywołanie MockUser
jej dziedziczenia z User
i MockUserService
.
Teraz, kiedy mamy zrobić MockUser.authenticate
i go z kolei domaga się super().validate_credentials()
MockUserService
to zanim UserService
w Metodzie Uchwałą Zakonu i, ponieważ oferuje implementacja beton z validate_credentials
tej implementacji zostaną wykorzystane. Tak - z powodzeniem wyśmiewaliśmy się UserService
w naszych testach jednostkowych. Pomyśl, że UserService
moż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 UserService
dotknię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 User
jest a UserService
lub User
jest 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?
Odpowiedzi:
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.
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:
zamiast używać prawdziwych poświadczeń i sprawdzać, czy parametry są poprawnie odbierane, być może z zapewnieniami (bo przecież jest to kod testowy.):
W przeciwnym razie wygląda na to, że to rozgryzłeś. Możesz zweryfikować MRO w następujący sposób:
I możesz sprawdzić, czy
MockUserService
ma on pierwszeństwo przedUserService
.źródło