DIP - принцип инверсии зависимостей

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

Низкоуровневые содержат утилитарную функциональность: обращение к базе данных, запросы к серверу, рендеринг DOM-элементов на странице.

Высокоуровневые содержат сложную, более абстрактную бизнес-логику. Они достаточно абстрактны, чтобы их можно было использовать в разных проектах: авторизация пользователей, валидация форм, отправка уведомлений.

В устойчивых системах высокоуровневые модули, как правило, не требуют обновления при изменении низкоуровневых. Добиться подобной устойчивости помогает принцип инверсии зависимостей.

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

  • Высокоуровневые модули не должны зависеть от низкоуровневых; оба типа должны зависеть от абстракций.
  • Абстракции не должны зависеть от деталей, детали должны зависеть от абстракций.
Пример
Программа имеет реализацию создания вложенности каталогов. Для этого используется класс RelationshipFolder, который хранит отношения папок: родитель, ребенок, сосед.

Метод Research позволяет вывести вложенные папки в каталог.
Программа хранения подкаталога диска
from dataclasses import dataclass
from enum import Enum
from typing import ClassVar


class Relationship(Enum):
    PARENT = 0
    CHILD = 1
    NEIGHBOR = 2


@dataclass
class Folder:
    name: str


@dataclass
class RelationshipFolder:
    relations: ClassVar = []

    def add_parrent_and_child(self, parent, child):
        self.relations.append((parent, Relationship.PARENT, child))
        self.relations.append((child, Relationship.CHILD, parent))


def Research(name_folder: str, relations: RelationshipFolder):
    for i in relations.relations:
        if i[0].name == name_folder and i[1] == Relationship.PARENT:
            print(f'Подпапки главного каталога {i[2].name}')


if __name__ == '__main__':
    root = Folder('C//:')
    program = Folder('Program')
    window = Folder('Window')

    relation = RelationshipFolder()
    relation.add_parrent_and_child(root, program)
    relation.add_parrent_and_child(root, window)

    Research('C//:', relation)
Проблема реализации
Метод Research опирается на знание реализации конструкции хранения зависимостей в виде списка. Если мы спланируем изменить способ хранения отношений, то функция сломается.

Это противоречит правилу инверсии зависимостей и создает сильную связь метода от входящего объекта. Принцип гласит, что функция не должна зависит от внутренней реализации объекта.
Решение
Решением данной проблемы является предоставления вспомогательных методов внутри низкоуровневого модуля (объекта).

Создаем абстрактный класс RelationshipBrowser, который будет регламентировать наличие метода поиска отношений папки с именем all_child_of.

Наследуем наш класс RelationshipFolder от RelationshipBrowser и опишем реализацию метода поиска отношений.

Перепишем метод вывода отношений в консоль.

Так понизилась зависимость метода вывода отношений и метода поиска подкаталога. То есть понизили зацепление модулей.
Измененная программа
from abc import ABC, abstractmethod
from dataclasses import dataclass
from enum import Enum
from typing import ClassVar


class Relationship(Enum):
    PARENT = 0
    CHILD = 1
    NEIGHBOR = 2


@dataclass
class Folder:
    name: str


@dataclass
class RelationshipBrowser(ABC):

    @abstractmethod
    def all_child_of(self, name_folder: str):
        pass


@dataclass
class RelationshipFolder(RelationshipBrowser):
    relations: ClassVar = []

    def add_parrent_and_child(self, parent, child):
        self.relations.append((parent, Relationship.PARENT, child))
        self.relations.append((child, Relationship.CHILD, parent))

    def all_child_of(self, name_folder: str):
        for i in self.relations:
            if i[0].name == name_folder and i[1] == Relationship.PARENT:
                yield i[2].name


def Research(name_folder: str, relations: RelationshipFolder):
    for i in relations.all_child_of(name_folder):
        print(f'Подпапки главного каталога {i}')


if __name__ == '__main__':
    root = Folder('C//:')
    program = Folder('Program')
    window = Folder('Window')

    relation = RelationshipFolder()
    relation.add_parrent_and_child(root, program)
    relation.add_parrent_and_child(root, window)

    Research('C//:', relation)
Вывод
Принцип инверсии зависимостей:

  • вводит правила и ограничения для зависимости одних модулей от других;
  • снижает зацепление модулей;
  • делает тестирование модулей проще;
  • позволяет проектировать систему так, чтобы модули были заменяемы на другие.