Sử dụng thứ tự độ phân giải phương pháp của Python để tiêm phụ thuộc - điều này có tệ không?


11

Tôi đã xem Pycon của Raymond Hettinger nói "Siêu được coi là siêu" và tìm hiểu một chút về MRO (Thứ tự giải quyết phương pháp) của Python để tuyến tính hóa các lớp "cha mẹ" theo cách xác định. Chúng ta có thể sử dụng điều này cho lợi thế của mình, như trong đoạn mã dưới đây, để thực hiện tiêm phụ thuộc. Vì vậy, bây giờ, tự nhiên, tôi muốn sử dụng supercho tất cả mọi thứ!

Trong ví dụ dưới đây, Userlớp khai báo các phụ thuộc của nó bằng cách kế thừa từ cả hai LoggingServiceUserService. Điều này không đặc biệt. Phần thú vị là chúng ta có thể sử dụng Lệnh giải quyết phương pháp cũng chế nhạo các phụ thuộc trong quá trình kiểm tra đơn vị. Mã dưới đây tạo ra một MockUserServicekế thừa từ đó UserServicevà cung cấp một triển khai các phương thức mà chúng ta muốn giả định. Trong ví dụ dưới đây, chúng tôi cung cấp một triển khai validate_credentials. Để MockUserServicexử lý bất kỳ cuộc gọi nào, validate_credentialschúng tôi cần định vị nó trước UserServicetrong MRO. Điều này được thực hiện bằng cách tạo một lớp bao bọc xung quanh Userđược gọi MockUservà có nó kế thừa từ UserMockUserService.

Bây giờ, khi chúng ta thực hiện MockUser.authenticatevà lần lượt, các cuộc gọi đến super().validate_credentials() MockUserServicetrước UserServicetrong Lệnh giải quyết phương pháp và, vì nó cung cấp một triển khai cụ thể của validate_credentialsviệc triển khai này sẽ được sử dụng. Yay - chúng tôi đã chế giễu thành công UserServicetrong các bài kiểm tra đơn vị của chúng tôi. Hãy xem xét điều đó UserServicecó thể thực hiện một số cuộc gọi mạng hoặc cơ sở dữ liệu đắt tiền - chúng tôi vừa loại bỏ yếu tố độ trễ của việc này. Cũng không có nguy cơ UserServicechạm vào dữ liệu trực tiếp / 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())

Điều này cảm thấy khá thông minh, nhưng đây có phải là cách sử dụng tốt và hợp lệ của nhiều kế thừa và Thứ tự giải quyết phương thức của Python không? Khi tôi nghĩ về sự kế thừa theo cách mà tôi đã học OOP với Java, điều này cảm thấy hoàn toàn sai lầm bởi vì chúng ta không thể nói Userlà một UserServicehoặc Userlà một LoggingService. Nghĩ theo cách đó, sử dụng tính kế thừa theo cách mà đoạn mã trên sử dụng nó không có ý nghĩa gì nhiều. Hoặc là nó? Nếu chúng ta sử dụng tính kế thừa hoàn toàn chỉ để cung cấp việc sử dụng lại mã và không suy nghĩ về mối quan hệ cha mẹ-> con cái, thì điều này có vẻ không tệ lắm.

Tôi đang làm sai à?


Có vẻ như có hai câu hỏi khác nhau ở đây: "Loại thao tác MRO này có an toàn / ổn định không?" và "Có chính xác không khi nói rằng mô hình thừa kế Python có mối quan hệ" is-a "?" Bạn đang cố gắng hỏi cả hai, hoặc chỉ một trong số họ? (cả hai đều là những câu hỏi hay, chỉ muốn đảm bảo chúng tôi trả lời đúng hoặc chia thành hai câu hỏi nếu bạn không muốn cả hai)
Ixrec

Tôi đã giải quyết các câu hỏi khi tôi đọc nó, tôi có để lại bất cứ điều gì không?
Aaron Hall

@lxrec Tôi nghĩ bạn hoàn toàn đúng. Tôi đang cố gắng hỏi hai câu hỏi khác nhau. Tôi nghĩ lý do điều này không cảm thấy "đúng" là vì tôi nghĩ về kiểu "thừa kế" (vì vậy GoldenRetriever "là" Chó và Chó "là" Động vật "thay vì loại này cách tiếp cận thành phần. Tôi nghĩ rằng đây là điều mà tôi có thể mở một câu hỏi khác cho :)
Iain

Điều này cũng làm tôi bối rối đáng kể. Nếu thành phần được ưu tiên kế thừa, tại sao không chuyển các phiên bản của LoggingService và UserService cho hàm tạo của Người dùng và đặt chúng làm thành viên? Sau đó, bạn có thể sử dụng kiểu gõ vịt để tiêm phụ thuộc và chuyển một thể hiện của MockUserService cho hàm tạo của Người dùng. Tại sao sử dụng siêu cho DI thích hợp hơn?
Jake Spracher

Câu trả lời:


7

Sử dụng thứ tự độ phân giải phương pháp của Python để tiêm phụ thuộc - điều này có tệ không?

Không. Đây là cách sử dụng dự định trên lý thuyết của thuật toán tuyến tính hóa C3. Điều này đi ngược lại các mối quan hệ quen thuộc của bạn, nhưng một số người coi thành phần được ưu tiên để kế thừa. Trong trường hợp này, bạn sáng tác một số mối quan hệ có một. Có vẻ như bạn đang đi đúng hướng (mặc dù Python có mô-đun ghi nhật ký, vì vậy ngữ nghĩa có chút nghi vấn, nhưng như một bài tập học thuật, nó hoàn toàn ổn).

Tôi không nghĩ việc nhạo báng hay vá khỉ là một điều xấu, nhưng nếu bạn có thể tránh chúng bằng phương pháp này, tốt cho bạn - với sự phức tạp hơn, bạn đã tránh sửa đổi các định nghĩa của lớp sản xuất.

Tôi đang làm sai à?

Nó có vẻ tốt. Bạn đã ghi đè một phương thức có khả năng đắt tiền, mà không cần vá khỉ hoặc sử dụng bản vá giả, điều đó, một lần nữa, có nghĩa là bạn thậm chí không trực tiếp sửa đổi các định nghĩa lớp sản xuất.

Nếu mục đích là để thực hiện chức năng mà không thực sự có thông tin xác thực trong thử nghiệm, có lẽ bạn nên làm một cái gì đó như:

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

thay vì sử dụng thông tin xác thực của bạn và kiểm tra xem các tham số có được nhận chính xác không, có lẽ với các xác nhận (vì đây là mã kiểm tra, sau tất cả.):

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

Nếu không, có vẻ như bạn đã tìm ra nó. Bạn có thể xác minh MRO như thế này:

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

Và bạn có thể xác minh rằng MockUserServiceđã được ưu tiên hơn UserService.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.