Використання порядку вирішення методу Python для введення залежності - це погано?


11

Я спостерігав, як Реймонд Хеттінгер говорив про "Супер вважається Супер" і дізнався трохи про MRO Python (Order Resolution Order), який детерміновано лінеаризує "батьківські" класи. Ми можемо використовувати це для наших переваг, як у наведеному нижче коді, щоб зробити ін'єкцію залежності. Тому зараз, природно, я хочу використовувати superдля всього!

У наведеному нижче прикладі Userклас заявляє про свої залежності, успадковуючи і від, LoggingServiceі від UserService. Це не особливо. Цікава частина полягає в тому, що ми можемо використовувати наказ про дозвіл методів також висміювати залежності під час тестування одиниць. Нижче наведений код створює функцію, MockUserServiceяка успадковується UserServiceта забезпечує реалізацію методів, з яких ми хочемо знущатися. У наведеному нижче прикладі ми пропонуємо реалізацію validate_credentials. Для того, щоб MockUserServiceобробляти будь-які дзвінки, validate_credentialsнам потрібно позиціонувати його раніше 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 та методу вирішення? Коли я думаю про спадкування в тому , як я дізнався ООП з Java це відчуває себе абсолютно неправильно , тому що ми не можемо сказати , Userце UserServiceабо Userце LoggingService. Думаючи про це, використовувати успадкування так, як використовується вищевказаний код, не має особливого сенсу. Або це? Якщо ми використовуємо спадщину виключно для того, щоб забезпечити повторне використання коду, а не думати про стосунки батьків-> дітей, то це не здається поганим.

Чи я це роблю неправильно?


Здається, що тут є два різні питання: "Чи безпечний / стабільний такий тип маніпуляцій з МРО?" і "Чи неправильно сказати, що наслідування Python моделює відносини" є-а "?" Ви намагаєтесь запитати обох, чи лише одного з них? (обидва вони хороші запитання, просто хочете переконатися, що ми відповіли правильний, або розділимо це на два запитання, якщо ви не хочете обох)
Ixrec

Коли я їх читав, я вирішив питання, чи щось не залишив?
Аарон Холл

@lxrec Я думаю, ви абсолютно праві. Я намагаюся задати два різні питання. Я думаю, що причина цього не вважається "правильним" - це те, що я думаю про стиль успадкування "є-а" (так що GoldenRetriever "- це" собака і собака "- це" тварина ") композиційний підхід. Я думаю, що це те, що я можу відкрити ще одне питання для :)
Iain

Це також мене значно бентежить. Якщо композиція є кращою для спадкування, чому б не передати екземпляри LoggingService та UserService конструктору користувача та встановити їх як членів? Тоді ви можете використовувати набір качок для введення залежності та надіслати екземпляр MockUserService замість конструктора користувача. Чому краще використовувати Super для DI?
Джейк Спрачер

Відповіді:


7

Використання порядку вирішення методу Python для введення залежності - це погано?

Ні. Це теоретичне призначення алгоритму лінеаризації С3. Це суперечить вашим знайомим відносинам, але деякі вважають композицію кращою для спадкування. У цьому випадку ви склали деякі стосунки. Здається, ви на правильному шляху (хоча 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.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.