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?