Использование порядка разрешения методов Python для внедрения зависимостей - это плохо?

11

Я наблюдал за речью Рэймонда Хеттингера о Pycon «Супер рассмотренный супер» и немного узнал о MRO (порядок разрешения методов) Python, который линеаризует классы «родительские» классы детерминистическим способом. Мы можем использовать это в наших интересах, как в приведенном ниже коде, для внедрения зависимостей. Так что теперь, естественно, я хочу использовать superдля всего!

В приведенном ниже примере Userкласс объявляет свои зависимости, наследуя от обоих LoggingServiceи UserService. Это не особенно особенное. Интересно то, что мы можем использовать Порядок разрешения методов и макетировать зависимости во время модульного тестирования. Код ниже создает объект, MockUserServiceкоторый наследует UserServiceи обеспечивает реализацию методов, которые мы хотим смоделировать. В приведенном ниже примере мы предоставляем реализацию validate_credentials. Для того, чтобы MockUserServiceобрабатывать любые вызовы, validate_credentialsнам нужно разместить его раньше UserServiceв MRO. Это делается путем создания класса-обертки для Userвызываемого MockUserи наследования от Userи MockUserService.

Теперь, когда мы это сделаем, MockUser.authenticateа это, в свою очередь, вызовы super().validate_credentials() MockUserService- это раньше, UserServiceв Порядке разрешения методов и, поскольку он предлагает конкретную реализацию validate_credentialsэтой реализации, будет использоваться. Yay - мы успешно смоделированы UserServiceв наших модульных тестах. Учтите, что это UserServiceможет сделать дорогостоящие вызовы сети или базы данных - мы только что убрали фактор задержки. Также нет риска UserServiceприкосновения к данным в реальном времени / продукту.

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

Это кажется довольно умным, но действительно ли это хорошее и правильное использование множественного наследования Python и порядка разрешения методов? Когда я думаю о наследовании способом, которым я изучил ООП с Java, это кажется совершенно неправильным, потому что мы не можем сказать, Userявляется UserServiceили Userесть LoggingService. Думать таким образом, используя наследование так, как это делает вышеприведенный код, не имеет особого смысла. Или это? Если мы используем наследование исключительно для обеспечения повторного использования кода и не думаем с точки зрения отношений между родителями и детьми, то это не так уж плохо.

Я делаю это неправильно?

Iain
источник
Кажется, здесь есть два разных вопроса: «Является ли этот вид манипуляции MRO безопасным / стабильным?» и "Неверно ли говорить, что наследование Python моделирует отношения" есть "?" Вы пытаетесь спросить обоих или только одного из них? (они оба хорошие вопросы, просто хотим убедиться, что мы ответим на правильный вопрос, или разбить их на два вопроса, если вы не хотите отвечать на оба
вопроса
Я отвечал на вопросы, когда я читал их, я что-то пропустил?
Аарон Холл
@lxrec Я думаю, ты абсолютно прав. Я пытаюсь задать два разных вопроса. Я думаю, что причина, по которой это не кажется «правильным», заключается в том, что я думаю о «это» стиле наследования (так что GoldenRetriever »это« собака и собака »это« животное ») вместо этого типа композиционный подход. Я думаю , что это то , что я мог бы открыть еще один вопрос для :)
Иэн
Это также сильно смущает меня. Если состав предпочтительнее наследования, почему бы не передать экземпляры LoggingService и UserService в конструктор User и установить их в качестве членов? Тогда вы можете использовать утку для внедрения зависимостей и вместо этого передать экземпляр MockUserService в конструктор User. Почему использование супер для DI предпочтительнее?
Джейк Спрейчер

Ответы:

7

Использование порядка разрешения методов Python для внедрения зависимостей - это плохо?

Нет. Это теоретическое использование алгоритма линеаризации С3. Это идет вразрез с вашими привычными отношениями is-a, но некоторые считают композицию предпочтительной для наследования. В этом случае вы создали некоторые отношения. Кажется, вы на правильном пути (хотя в Python есть модуль журналирования, поэтому семантика немного сомнительна, но в качестве академического упражнения это совершенно нормально).

Я не думаю, что издевательство или исправление обезьян - это плохо, но если вы можете избежать их с помощью этого метода, полезного для вас - по общему признанию, более сложного, вы избежали изменения определений производственного класса.

Я делаю это неправильно?

Это выглядит хорошо. Вы переопределили потенциально дорогой метод, не применяя обезьяньего патча или фиктивного патча, что, опять же, означает, что вы даже напрямую не изменили определения производственного класса.

Если цель состояла в том, чтобы использовать функциональность, не имея учетных данных в тесте, вы, вероятно, должны сделать что-то вроде:

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

вместо того, чтобы использовать свои реальные учетные данные, и проверьте, что параметры получены правильно, возможно, с утверждениями (поскольку это, в конце концов, тестовый код):

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

В противном случае, похоже, что вы поняли это. Вы можете проверить MRO следующим образом:

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

И вы можете проверить, что MockUserServiceимеет приоритет над UserService.

Аарон Холл
источник