Menggunakan Orde Resolusi Metode Python untuk Injeksi Ketergantungan - apakah ini buruk?


11

Saya menonton pembicaraan Pycon karya Raymond Hettinger, "Super Considered Super" dan belajar sedikit tentang Python MRO (Method Resolution Order) yang membuat linearkan kelas "induk" kelas dengan cara deterministik. Kita dapat menggunakan ini untuk keuntungan kita, seperti dalam kode di bawah ini, untuk melakukan injeksi ketergantungan. Jadi sekarang, tentu saja, saya ingin menggunakannya superuntuk semuanya!

Dalam contoh di bawah ini, Userkelas menyatakan dependensinya dengan mewarisi dari keduanya LoggingServicedan UserService. Ini tidak terlalu istimewa. Bagian yang menarik adalah bahwa kita dapat menggunakan Metode Resolution Order juga mengejek dependensi selama pengujian unit. Kode di bawah ini membuat MockUserServiceyang diturunkan dari UserServicedan menyediakan implementasi metode yang ingin kita tiru. Dalam contoh di bawah ini, kami menyediakan implementasi validate_credentials. Agar dapat MockUserServicemenangani panggilan apa pun, validate_credentialskita perlu UserServicemengaturnya sebelum di MRO. Ini dilakukan dengan membuat kelas pembungkus sekitar Userdipanggil MockUserdan memilikinya mewarisi dari Userdan MockUserService.

Sekarang, ketika kita melakukannya MockUser.authenticatedan, pada gilirannya, panggilan ke super().validate_credentials() MockUserServicesebelumnya UserServicedalam Metode Resolusi Urutan dan, karena menawarkan implementasi konkret dari validate_credentialsimplementasi ini akan digunakan. Yay - kami berhasil mengejek UserServiceunit test kami. Pertimbangkan yang UserServicemungkin melakukan panggilan jaringan atau database yang mahal - kami baru saja menghapus faktor latensi ini. Juga tidak ada risiko UserServicemenyentuh data langsung / prod.

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

Ini terasa cukup pintar, tetapi apakah ini penggunaan yang baik dan valid dari pewarisan berganda Python dan Metode Resolution Order? Ketika saya berpikir tentang pewarisan cara saya belajar OOP dengan Java, ini terasa sangat salah karena kita tidak bisa mengatakan Userapakah a UserServiceatau Usera LoggingService. Berpikir seperti itu, menggunakan warisan cara kode di atas menggunakannya tidak masuk akal. Atau itu? Jika kita menggunakan warisan semata-mata hanya untuk menyediakan penggunaan kembali kode, dan tidak memikirkan hubungan orangtua-anak, maka ini sepertinya tidak terlalu buruk.

Apakah saya salah?


Sepertinya ada dua pertanyaan berbeda di sini: "Apakah manipulasi MRO semacam ini aman / stabil?" dan "Apakah tidak tepat untuk mengatakan bahwa model pewarisan Python merupakan hubungan" is-a "?" Apakah Anda mencoba menanyakan keduanya, atau hanya salah satunya? (Keduanya adalah pertanyaan yang bagus, hanya ingin memastikan kami menjawab yang benar, atau membaginya menjadi dua pertanyaan jika Anda tidak menginginkan keduanya)
Ixrec

Saya telah menjawab pertanyaan saat saya membacanya, apakah saya meninggalkan sesuatu?
Aaron Hall

@ lxrec Saya pikir Anda memang benar. Saya mencoba mengajukan dua pertanyaan berbeda. Saya pikir alasan mengapa ini tidak terasa "benar" adalah karena saya berpikir tentang "adalah-" gaya pewarisan (jadi GoldenRetriever "adalah-" Anjing dan Anjing "adalah-" Hewan) daripada jenis pendekatan komposisi. Saya pikir ini adalah sesuatu yang saya bisa membuka pertanyaan lain untuk :)
Iain

Ini juga membingungkan saya secara signifikan. Jika komposisi lebih disukai daripada warisan mengapa tidak meneruskan instance LoggingService dan UserService ke konstruktor Pengguna dan menetapkannya sebagai anggota? Kemudian Anda bisa menggunakan bebek mengetik untuk injeksi ketergantungan dan meneruskan contoh MockUserService ke konstruktor pengguna sebagai gantinya. Mengapa menggunakan super untuk DI lebih disukai?
Jake Spracher

Jawaban:


7

Menggunakan Orde Resolusi Metode Python untuk Injeksi Ketergantungan - apakah ini buruk?

Tidak. Ini adalah tujuan teoritis dari algoritma linierisasi C3. Ini bertentangan dengan hubungan akrab Anda-a, tetapi beberapa menganggap komposisi lebih disukai daripada warisan. Dalam hal ini, Anda membuat beberapa hubungan has-a. Tampaknya Anda berada di jalur yang benar (meskipun Python memiliki modul logging, jadi semantiknya agak dipertanyakan, tetapi sebagai latihan akademis, itu tidak masalah).

Saya tidak berpikir mengejek atau menambal monyet adalah hal yang buruk, tetapi jika Anda dapat menghindarinya dengan metode ini, bagus untuk Anda - dengan kompleksitas yang lebih jelas, Anda telah menghindari memodifikasi definisi kelas produksi.

Apakah saya salah?

Itu terlihat bagus. Anda telah menimpa metode yang berpotensi mahal, tanpa tambalan monyet atau menggunakan tambalan tiruan, yang, sekali lagi, berarti Anda bahkan belum secara langsung mengubah definisi kelas produksi.

Jika maksudnya adalah untuk menjalankan fungsi tanpa benar-benar memiliki kredensial dalam pengujian, Anda mungkin harus melakukan sesuatu seperti:

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

alih-alih menggunakan kredensial Anda yang sebenarnya, dan periksa apakah parameternya diterima dengan benar, mungkin dengan pernyataan (setelah semua ini adalah kode tes.):

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

Kalau tidak, sepertinya Anda sudah menemukannya. Anda dapat memverifikasi MRO seperti ini:

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

Dan Anda dapat memverifikasi bahwa MockUserServicetelah didahulukan dari UserService.

Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.