Статьи
September 19, 2022

Принцип инверсии зависимостей (DIP)

Вот он — последний из пяти принципов ООП, венец коллекции, гвоздь программы. Ладно, другие принципы тоже важны:

S — принцип единой ответственности (Single responsibility Principle)

O — принцип открытости / закрытости (Open-closed Principle)

L — принцип подстановки Барбары Лисков (Liskov Substitution Principle)

I — принцип разделения интерфейса (Interface Segregation Principle)

D — принцип инверсии зависимостей (Dependency Inversion Principle)

Принцип инверсии зависимостей гласит:

  • Модули высокого уровня не должны зависеть от модулей низкого уровня. И те, и другие должны зависеть от абстракций.
  • Абстракции не должны зависеть от деталей. Детали должны зависеть от абстракций.

Принцип инверсии зависимостей направлен на уменьшение связи между классами путем создания между ними слоя абстракции.

Как вам, а? Погнали разбираться, кто что курил.

Рассмотрим пример:

class FXConverter:
    def convert(self, from_currency, to_currency, amount):
        print(f'{amount} {from_currency} = {amount * 1.2} {to_currency}')
        return amount * 1.2


class App:
    def start(self):
        converter = FXConverter()
        converter.convert('EUR', 'USD', 100)


if __name__ == '__main__':
    app = App()
    app.start()

Есть два класса: FXConverter и App. Представим, что класс FXConverter использует API стороннего конвертора для перевода суммы из одной валюты в другую. Для простоты мы жестко закодировали обменный курс как 1,2. На практике для получения обменного курса вам придется обратиться к API.

У класса App есть метод start(), который использует экземпляр класса FXconverter для конвертации 100 EUR в USD.

App — это модуль, который в значительной степени зависит от класса FXConverter, который, в свою очередь, зависит от API конвертора.

В будущем, если API конвертора изменится, это приведет к поломке кода. Также, если вы захотите использовать другой API, вам нужно будет изменить класс App.

Чтобы всего этого избежать, необходимо инвертировать зависимость таким образом, чтобы класс FXConverter адаптировался к классу App.

Для этого вы определяете интерфейс и делаете App зависимым от него, а не от класса FXConverter. Затем вы изменяете FXConverter так, чтобы он соответствовал интерфейсу.

Сначала определите абстрактный класс CurrencyConverter, который будет действовать как интерфейс. Пусть у него будет метод convert(), который должны реализовывать все подклассы CurrencyConverter:

from abc import ABC

class CurrencyConverter(ABC):
    def convert(self, from_currency, to_currency, amount) -> float:
        pass

Во-вторых, переопределите класс FXConverter так, чтобы он наследовался от класса CurrencyConverter и реализовал метод convert():

class FXConverter(CurrencyConverter):
    def convert(self, from_currency, to_currency, amount) -> float:
        print('Converting currency using FX API')
        print(f'{amount} {from_currency} = {amount * 1.2} {to_currency}')
        return amount * 2

В-третьих, добавьте метод __init__ в класс App и инициализируйте объект CurrencyConverter:

class App:
    def __init__(self, converter: CurrencyConverter):
        self.converter = converter

    def start(self):
        self.converter.convert('EUR', 'USD', 100)

Теперь класс App зависит от интерфейса CurrencyConverter, а не от класса FXConverter.

Ниже пример создания экземпляра FXConverter и его передачи в App:

if __name__ == '__main__':
    converter = FXConverter()
    app = App(converter)
    app.star

Вывод:

Converting currency using FX API
100 EUR = 120.0 USD

В будущем вы можете поддерживать API другого конвертера валют, создав подкласс CurrencyConverter. Например, ниже определен класс AlphaConverter, который наследуется от CurrencyConverter.

class AlphaConverter(CurrencyConverter):
    def convert(self, from_currency, to_currency, amount) -> float:
        print('Converting currency using Alpha API')
        print(f'{amount} {from_currency} = {amount * 1.2} {to_currency}')
        return amount * 1.15

Поскольку класс AlphaConvert наследуется от класса CurrencyConverter, вы можете использовать его объект в классе App без изменения последнего:

if __name__ == '__main__':
    converter = AlphaConverter()
    app = App(converter)
    app.start()

Вывод:

Converting currency using Alpha API
100 EUR = 120.0 USD

Собираем всё в кучку:

from abc import ABC


class CurrencyConverter(ABC):
    def convert(self, from_currency, to_currency, amount) -> float:
        pass


class FXConverter(CurrencyConverter):
    def convert(self, from_currency, to_currency, amount) -> float:
        print('Converting currency using FX API')
        print(f'{amount} {from_currency} = {amount * 1.2} {to_currency}')
        return amount * 1.15


class AlphaConverter(CurrencyConverter):
    def convert(self, from_currency, to_currency, amount) -> float:
        print('Converting currency using Alpha API')
        print(f'{amount} {from_currency} = {amount * 1.2} {to_currency}')
        return amount * 1.2


class App:
    def __init__(self, converter: CurrencyConverter):
        self.converter = converter

    def start(self):
        self.converter.convert('EUR', 'USD', 100)


if __name__ == '__main__':
    converter = AlphaConverter()
    app = App(converter)
    app.start()

Заключение

Используйте принцип инверсии зависимостей, чтобы сделать ваш код более надежным. Делайте высокоуровневый модуль зависимым от абстракции, а не от конкретной реализации.

Источник: Python Tutorial
Перевод и адаптация: Екатерина Прохорова

👉🏻Подписывайтесь на PythonTalk в Telegram 👈🏻

👨🏻‍💻Чат PythonTalk в Telegram💬

🍩 Поддержать канал 🫶