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


11

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

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

これを実行するMockUser.authenticateと、次に、への呼び出しがメソッド解決順序のsuper().validate_credentials() MockUserServiceUserServiceにあり、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を学んだ方法で、継承について考えるとき、我々が言うことができないので、完全に間違ってこの感触はUserあるUserServiceUserですLoggingService。そのように考えると、上記のコードが使用する方法で継承を使用することは、あまり意味がありません。またはそれは?コードの再利用を提供するためだけに継承を使用し、親と子の関係を考慮しない場合、これはそれほど悪くないように見えます。

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


ここには2つの異なる質問があるようです:「この種のMRO操作は安全/安定ですか?」「Pythonの継承が「is-a」関係をモデル化していると言うのは不正確ですか?」あなたはそれらの両方に尋ねようとしていますか、それともそれらの一方だけに尋ねようとしていますか?(どちらも良い質問です。正しい質問に回答することを確認するか、両方とも必要ない場合は2つの質問に分けてください)
Ixrec

質問を読みながら読み終えましたが、何か省略しましたか?
アーロンホール

@lxrec私はあなたが絶対的に正しいと思います。私は2つの異なる質問をしようとしています。これが「正しい」と感じられない理由は、このタイプの継承ではなく、「is-a」スタイルの継承(つまり、GoldenRetrieverの「is-a」犬と犬の「is-a」動物)について考えているからです。組成アプローチ。これは、別の質問を開くことができるものだと思います:)
Iain

これも私をかなり混乱させます。継承より構成の方が望ましい場合は、LoggingServiceおよびUserServiceのインスタンスをUserのコンストラクターに渡して、それらをメンバーとして設定しないでください。次に、依存関係の注入にダックタイピングを使用して、代わりにMockUserServiceのインスタンスをUserコンストラクターに渡すことができます。DIにsuperを使用することが望ましいのはなぜですか?
Jake Spracher、2018

回答:


7

依存性注入に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'>]

また、MockUserServiceがに優先することを確認できますUserService

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.