Перейти к содержанию

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

Принцип инверсии зависимостей (Dependency Inversion Principle) гласит:

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

Если ваш код уже реализует принципы открытости/закрытости и подстановки Лисков, он уже будет неявно согласован с принципом инверсии зависимостей. Следуя принципу открытости/закрытости, вы создаете интерфейсы, которые можно использовать для предоставления различных высокоуровневых реализаций. Следуя принципу подстановки Лисков, вы гарантируете, что сможете заменить экземпляры класса низкого уровня объектами класса высокого уровня без какого-либо негативного воздействия на приложение. Таким образом, следуя этим двум принципам, вы гарантируете, что ваши классы высокого уровня и классы низкого уровня зависят от интерфейсов. Следовательно, вы неявно следуете принципу инверсии зависимостей. Как показано в коде ниже, у нас есть класс Student, который мы используем для создания экземпляров Student и класса TeamMemberships, который содержит сведения о принадлежности учеников к разным командам. Теперь мы определим высокоуровневый класс Analysis, где нам нужно отсеять всех учеников, принадлежащих красной команде.

from enum import Enum
from abc import abstractmethod

class Teams(Enum):
    BLUE_TEAM = 1
    RED_TEAM = 2
    GREEN_TEAM = 3

class Student:
    def __init__(self, name):
        self.name = name

class TeamMemberships():
    def __init__(self):
        self.team_memberships = []

    def add_team_memberships(self, student, team):
        self.team_memberships.append((student, team))

class Analysis():
    def __init__(self, team_student_memberships):
        memberships = team_student_memberships.team_memberships
        for members in memberships:
            if members[1] == Teams.RED_TEAM:
                print(f'{members[0].name} is in RED team')


student1 = Student('Ravi')
student2 = Student('Archie')
student3 = Student('James')

team_memberships = TeamMemberships()
team_memberships.add_team_memberships(student1, Teams.BLUE_TEAM)
team_memberships.add_team_memberships(student2, Teams.RED_TEAM)
team_memberships.add_team_memberships(student3, Teams.GREEN_TEAM)

Analysis(team_memberships)
<?php
enum Teams
{
    case BLUE_TEAM;
    case RED_TEAM;
    case GREEN_TEAM;
}

class Student {
    public string $name;

    public function __construct(string $name) {
        $this->name = $name;
    }
}

class TeamMemberships {
    public array $team_memberships = [];

    public function add_team_memberships(Student $student, Teams $team) {
        $this->team_memberships[] = [$student, $team];
    }
}

class Analysis {
    public function __construct(TeamMemberships $team_student_memberships) {
        $memberships = $team_student_memberships->team_memberships;
        foreach ($memberships as $member) {
            if ($member[1] === Teams::RED_TEAM) {
                echo $member[0]->name . ' is in RED team';
            }
        }
    }
}

$student1 = new Student('Ravi');
$student2 = new Student('Archie');
$student3 = new Student('James');

$team_memberships = new TeamMemberships();
$team_memberships->add_team_memberships($student1, Teams::BLUE_TEAM);
$team_memberships->add_team_memberships($student2, Teams::RED_TEAM);
$team_memberships->add_team_memberships($student3, Teams::GREEN_TEAM);

new Analysis($team_memberships);
Archie is in RED team

Задачи

Задача 1. Отправка уведомлений

Класс OrderService напрямую создаёт экземпляр EmailSender и вызывает его метод. Нарушает ли это DIP? Перепроектируйте так, чтобы OrderService зависел от абстракции.

# Нарушение DIP — модуль высокого уровня зависит от конкретной реализации
class EmailSender:
    def send(self, recipient: str, message: str):
        print(f"Email → {recipient}: {message}")

class OrderService:
    def __init__(self):
        self.notifier = EmailSender()  # жёсткая зависимость

    def place_order(self, order_id: int, email: str):
        print(f"Заказ #{order_id} оформлен")
        self.notifier.send(email, f"Ваш заказ #{order_id} принят")

# Вызов
service = OrderService()
service.place_order(101, "user@example.com")
<?php
// Нарушение DIP — модуль высокого уровня зависит от конкретной реализации
class EmailSender {
    public function send(string $recipient, string $message): void {
        echo "Email → {$recipient}: {$message}\n";
    }
}

class OrderService {
    private EmailSender $notifier;

    public function __construct() {
        $this->notifier = new EmailSender(); // жёсткая зависимость
    }

    public function place_order(int $order_id, string $email): void {
        echo "Заказ #{$order_id} оформлен\n";
        $this->notifier->send($email, "Ваш заказ #{$order_id} принят");
    }
}

// Вызов
$service = new OrderService();
$service->place_order(101, "user@example.com");

Задача 2. Логирование в приложении

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

# Нарушение DIP
class FileLogger:
    def log(self, message: str):
        print(f"[FILE] {message}")

class UserService:
    def __init__(self):
        self.logger = FileLogger()  # жёсткая зависимость

    def register(self, username: str):
        print(f"Пользователь {username} зарегистрирован")
        self.logger.log(f"Регистрация: {username}")

    def delete(self, username: str):
        print(f"Пользователь {username} удалён")
        self.logger.log(f"Удаление: {username}")

# Вызов
service = UserService()
service.register("Алексей")
service.delete("Алексей")
<?php
// Нарушение DIP
class FileLogger {
    public function log(string $message): void {
        echo "[FILE] {$message}\n";
    }
}

class UserService {
    private FileLogger $logger;

    public function __construct() {
        $this->logger = new FileLogger(); // жёсткая зависимость
    }

    public function register(string $username): void {
        echo "Пользователь {$username} зарегистрирован\n";
        $this->logger->log("Регистрация: {$username}");
    }

    public function delete(string $username): void {
        echo "Пользователь {$username} удалён\n";
        $this->logger->log("Удаление: {$username}");
    }
}

// Вызов
$service = new UserService();
$service->register("Алексей");
$service->delete("Алексей");

Задача 3. Генерация отчётов

Класс ReportGenerator напрямую зависит от PdfRenderer. Необходимо добавить поддержку HTML-рендеринга. Перепроектируйте с применением DIP.

# Нарушение DIP
class PdfRenderer:
    def render(self, content: str) -> str:
        return f"[PDF] {content}"

class ReportGenerator:
    def __init__(self):
        self.renderer = PdfRenderer()  # жёсткая зависимость

    def generate(self, data: str) -> str:
        content = f"Отчёт: {data}"
        return self.renderer.render(content)

# Вызов
generator = ReportGenerator()
print(generator.generate("Продажи за март"))
<?php
// Нарушение DIP
class PdfRenderer {
    public function render(string $content): string {
        return "[PDF] {$content}";
    }
}

class ReportGenerator {
    private PdfRenderer $renderer;

    public function __construct() {
        $this->renderer = new PdfRenderer(); // жёсткая зависимость
    }

    public function generate(string $data): string {
        $content = "Отчёт: {$data}";
        return $this->renderer->render($content);
    }
}

// Вызов
$generator = new ReportGenerator();
echo $generator->generate("Продажи за март") . PHP_EOL;

Задача 4. Подключение к базе данных

Класс ProductRepository создаёт внутри себя MySQLConnection. Перепроектируйте так, чтобы репозиторий зависел от абстракции соединения, и можно было легко подключить PostgreSQL.

# Нарушение DIP
class MySQLConnection:
    def query(self, sql: str) -> list:
        print(f"[MySQL] Выполнение: {sql}")
        return [{'id': 1, 'name': 'Товар A'}]

class ProductRepository:
    def __init__(self):
        self.db = MySQLConnection()  # жёсткая зависимость

    def find_all(self) -> list:
        return self.db.query("SELECT * FROM products")

    def find_by_id(self, id: int) -> dict:
        results = self.db.query(f"SELECT * FROM products WHERE id = {id}")
        return results[0] if results else {}

# Вызов
repo = ProductRepository()
print(repo.find_all())
print(repo.find_by_id(1))
<?php
// Нарушение DIP
class MySQLConnection {
    public function query(string $sql): array {
        echo "[MySQL] Выполнение: {$sql}\n";
        return [['id' => 1, 'name' => 'Товар A']];
    }
}

class ProductRepository {
    private MySQLConnection $db;

    public function __construct() {
        $this->db = new MySQLConnection(); // жёсткая зависимость
    }

    public function find_all(): array {
        return $this->db->query("SELECT * FROM products");
    }

    public function find_by_id(int $id): array {
        $results = $this->db->query("SELECT * FROM products WHERE id = {$id}");
        return $results[0] ?? [];
    }
}

// Вызов
$repo = new ProductRepository();
print_r($repo->find_all());
print_r($repo->find_by_id(1));

Задача 5. Кэширование данных

Класс ArticleService напрямую использует RedisCache для кэширования статей. Перепроектируйте с введением абстракции кэша, чтобы можно было подменить Redis на in-memory кэш в тестах.

# Нарушение DIP
class RedisCache:
    def __init__(self):
        self._store = {}

    def get(self, key: str):
        return self._store.get(key)

    def set(self, key: str, value):
        self._store[key] = value
        print(f"[Redis] Сохранено: {key}")

class ArticleService:
    def __init__(self):
        self.cache = RedisCache()  # жёсткая зависимость

    def get_article(self, article_id: int) -> dict:
        key = f"article:{article_id}"
        cached = self.cache.get(key)
        if cached:
            return cached
        article = {'id': article_id, 'title': f'Статья #{article_id}'}
        self.cache.set(key, article)
        return article

# Вызов
service = ArticleService()
print(service.get_article(1))
print(service.get_article(1))  # из кэша
<?php
// Нарушение DIP
class RedisCache {
    private array $store = [];

    public function get(string $key): mixed {
        return $this->store[$key] ?? null;
    }

    public function set(string $key, mixed $value): void {
        $this->store[$key] = $value;
        echo "[Redis] Сохранено: {$key}\n";
    }
}

class ArticleService {
    private RedisCache $cache;

    public function __construct() {
        $this->cache = new RedisCache(); // жёсткая зависимость
    }

    public function get_article(int $article_id): array {
        $key = "article:{$article_id}";
        $cached = $this->cache->get($key);
        if ($cached !== null) {
            return $cached;
        }
        $article = ['id' => $article_id, 'title' => "Статья #{$article_id}"];
        $this->cache->set($key, $article);
        return $article;
    }
}

// Вызов
$service = new ArticleService();
print_r($service->get_article(1));
print_r($service->get_article(1)); // из кэша

Как видно из реализации, мы напрямую используем team_student_memberships. team_memberships в высокоуровневом классе Analysis, и мы используем реализацию этого списка непосредственно в классе высокого уровня. На данный момент все нормально, но представьте ситуацию, в которой нам нужно изменить эту реализацию со списка на что-то другое. В этом случае наш класс высокого уровня Analysis сломается, поскольку он зависит от деталей реализации TeamMemberships низкого уровня. Теперь взгляните на пример ниже, в котором мы меняем эту реализацию и приводим ее в соответствие с принципом инверсии зависимостей.

from enum import Enum
from abc import abstractmethod

class Teams(Enum):
    BLUE_TEAM = 1
    RED_TEAM = 2
    GREEN_TEAM = 3


class TeamMembershipLookup():
    @abstractmethod
    def find_all_students_of_team(self, team):
        pass

class Student:
    def __init__(self, name):
        self.name = name

class TeamMemberships(TeamMembershipLookup):
    def __init__(self):
        self.team_memberships = []

    def add_team_memberships(self, student, team):
        self.team_memberships.append((student, team))

    def find_all_students_of_team(self, team):
        for members in self.team_memberships:
            if members[1] == team:
                yield members[0].name

class Analysis():
    def __init__(self, team_membership_lookup):
        for student in team_membership_lookup.find_all_students_of_team(Teams.RED_TEAM):
            print(f'{student} is in RED team.')


student1 = Student('Ravi')
student2 = Student('Archie')
student3 = Student('James')

team_memberships = TeamMemberships()
team_memberships.add_team_memberships(student1, Teams.BLUE_TEAM)
team_memberships.add_team_memberships(student2, Teams.RED_TEAM)
team_memberships.add_team_memberships(student3, Teams.GREEN_TEAM)

Analysis(team_memberships)
<?php
enum Teams
{
    case BLUE_TEAM;
    case RED_TEAM;
    case GREEN_TEAM;
}

interface TeamMembershipLookup {
    public function find_all_students_of_team(Teams $team);
}

class Student {
    public string $name;

    public function __construct(string $name) {
        $this->name = $name;
    }
}

class TeamMemberships implements TeamMembershipLookup {
    private array $team_memberships = [];

    public function add_team_memberships(Student $student, Teams $team) {
        $this->team_memberships[] = [$student, $team];
    }

    public function find_all_students_of_team(Teams $team) {
        return array_map(
            function ($item) {
                return $item[0]->name;
            },
            array_filter(
                $this->team_memberships,
                function ($item) use ($team) {
                    return $item[1] === $team;
                }
            )
        );
    }
}

class Analysis {
    public function __construct(TeamMemberships $team_student_memberships) {
        $memberships = $team_student_memberships->find_all_students_of_team(Teams::RED_TEAM);
        foreach ($memberships as $member) {
            echo $member . ' is in RED team';
        }
    }
}

$student1 = new Student('Ravi');
$student2 = new Student('Archie');
$student3 = new Student('James');

$team_memberships = new TeamMemberships();
$team_memberships->add_team_memberships($student1, Teams::BLUE_TEAM);
$team_memberships->add_team_memberships($student2, Teams::RED_TEAM);
$team_memberships->add_team_memberships($student3, Teams::GREEN_TEAM);

new Analysis($team_memberships);