使用Python的方法解析顺序进行依赖注入-这样不好吗?


11

我观看了Raymond Hettinger在Pycon上发表的“超级被认为是超级”的演讲,并了解了一些Python的MRO(方法解析顺序),该方法以确定性方式线性化了“父”类。像下面的代码一样,我们可以利用它来发挥优势,进行依赖注入。因此,自然而然地,我现在想使用super所有功能!

在下面的示例中,User该类通过从LoggingService和继承来声明其依赖项UserService。这不是特别特殊。有趣的是,我们可以使用“方法解析顺序”来模拟单元测试期间的依赖关系。下面的代码创建一个MockUserService从继承UserService并提供我们要模拟的方法的实现。在下面的示例中,我们提供的实现validate_credentials。为了MockUserService处理任何呼叫,validate_credentials我们需要先将其UserService放置在MRO中。这通过创建一个包装类各地进行User所谓的MockUser和有它继承UserMockUserService

现在,当我们执行MockUser.authenticate该操作时,它依次调用方法解析顺序中的super().validate_credentials() MockUserServicebefore UserService,因为它提供了validate_credentials该实现的具体实现。是的-我们已经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学习OOP的方式来考虑继承时,这感觉是完全错误的,因为我们不能说Usera UserServiceUseris a LoggingService。这样想,以上述代码使用继承的方式使用继承没有多大意义。还是?如果我们仅使用继承来提供代码重用,而不考虑父子关系,那么这似乎并不糟糕。

我做错了吗?


似乎这里有两个不同的问题:“这种MRO操作安全/稳定吗?” 以及“说Python继承为“ is-a”关系建模是否不正确?” 您是要问这两个问题,还是只问其中之一?(它们都是好问题,只是要确保我们回答正确的问题,或者如果您不想两者都回答,则将其分为两个问题)
Ixrec

在阅读时,我已经解决了问题,是否遗漏了任何内容?
亚伦·霍尔

@lxrec我认为你是绝对正确的。我想问两个不同的问题。我认为这种感觉不是“正确”的原因是因为我正在考虑继承“ is-a”样式(所以GoldenRetriever是“ is-a” Dog and Dog“ is-a” Animal)而不是这种类型的合成方法。我认为这是我可以提出的另一个问题:)
Iain

这也使我非常困惑。如果组合比继承更可取,为什么不将LoggingService和UserService实例传递给User的构造函数并将它们设置为成员呢?然后,您可以使用鸭子类型进行依赖项注入,并将MockUserService的实例传递给User构造函数。为什么对DI使用super更好?
杰克·斯普拉彻

Answers:


7

使用Python的方法解析顺序进行依赖注入-这样不好吗?

否。这是C3线性化算法的理论预期用途。这违背了您熟悉的is-a关系,但有些人认为组合比继承更受青睐。在这种情况下,您组成了一些has-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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.