I watched Raymond Hettinger's Pycon talk "Super Considered Super" and learned a little bit about Python's MRO (Method Resolution Order) which linearises a classes "parent" classes in a deterministic way. We can use this to our advantage, like in the below code, to do dependency injection. So now, naturally, I want to use super
for everything!
In the example below, the User
class declares it's dependencies by inheriting from both LoggingService
and UserService
. This isn't particularly special. The interesting part is that we can use the Method Resolution Order also mock out dependencies during unit testing. The code below creates a MockUserService
which inherits from UserService
and provides an implementation of the methods we want to mock. In the example below, we provide an implementation of validate_credentials
. In order to have MockUserService
handle any calls to validate_credentials
we need to position it before UserService
in the MRO. This done by creating a wrapper class around User
called MockUser
and having it inherit from User
and MockUserService
.
Now, when we do MockUser.authenticate
and it, in turn, calls to super().validate_credentials()
MockUserService
is before UserService
in the Method Resolution Order and, since it offers a concrete implementation of validate_credentials
this implementation will be used. Yay - we've successfully mocked out UserService
in our unit tests. Consider that UserService
might do some expensive network or database calls - we've just removed the latency factor of this. There is also no risk of UserService
touching live/prod data.
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())
This feels quite clever, but is this a good and valid use of Python's multiple inheritance and Method Resolution Order? When I think about inheritance in the way that I learned OOP with Java this feels completely wrong because we can't say User
is a UserService
or User
is a LoggingService
. Thinking that way, using inheritance the way the above code uses it doesn't make much sense. Or is it? If we use inheritance purely just to provide code reuse, and not thinking in terms of parent->children relationships, then this doesn't seem so bad.
Am I doing it wrong?