web-dev-qa-db-ja.com

依存性注入にPythonのメソッド解決順序を使用する-これは悪いことですか?

私はレイモンドヘッティンガーのPyconの講演「超熟考スーパー」を見て、決定論的な方法でクラス「親」クラスを線形化するPythonのMRO(メソッド解決順序)について少し学びました。これを使用して、以下のコードのように、依存性注入を行うことができます。だから今、当然、すべてにsuperを使用したいと思います!

以下の例では、Userクラスは、LoggingServiceUserServiceの両方から継承することで、依存関係を宣言しています。これは特に特別なことではありません。興味深いのは、メソッド解決順序を使用して、単体テスト中に依存関係を模擬できることです。以下のコードは、MockUserServiceを継承するUserServiceを作成し、モックしたいメソッドの実装を提供します。以下の例では、_validate_credentials_の実装を提供しています。 MockUserServiceに_validate_credentials_への呼び出しを処理させるには、MROでUserServiceの前に配置する必要があります。これは、Userと呼ばれるMockUserの周りにラッパークラスを作成し、UserおよびMockUserServiceから継承させることで行われます。

ここで、_MockUser.authenticate_を実行すると、次に、super().validate_credentials()MockUserServiceの呼び出しが、メソッド解決順序の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の多重継承とメソッド解決順序の有効かつ有効な使用ですか私が学んだ方法で継承について考えるときOOP with Javaこれは、UserUserServiceまたはUserLoggingServiceです。そのように考えると、上記のコードのように継承を使用しても意味がありません。あるいは、継承を使用する場合純粋にコードの再利用を提供するためだけで、親と子の関係を考慮せずに、これはそれほど悪くないように見えます。

私はそれを間違っていますか?

11
Iain

依存関係注入のためのPythonのメソッド解決順序の使用-これは悪いですか?

いいえ。これは、C3線形化アルゴリズムの理論的な使用目的です。これはおなじみのis-a関係に反しますが、継承よりも構成を優先すると考える人もいます。この場合は、いくつかのa-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'>]

そして、MockUserServiceUserServiceよりも優先されていることを確認できます。

7
Aaron Hall