Чистый код: Инверсия зависимостей (DIP)

Инверсия зависимостей - это стратегия зависимости от интерфейсов или абстрактных функций и классов, а не от конкретных функций и классов. Проще говоря, когда компоненты нашей системы имеют зависимости, мы не хотим напрямую внедрять зависимость одного компонента в другой. Вместо этого мы должны использовать определенный уровень абстракции между ними.

Вот такое сложное определение. Где, «Проще говоря», только больше запутывает чем поясняет, хотя все передает верно. Этот принцип пожалуй самый сложный для объяснений, хотя его суть очевидна.

Начну пояснения с бытового примера. Для работы лампы освещение необходим провод, ну и электричество естественно. Логичным является, встроить эту лампу в светильник, который будет играть двойную роль, защитную и декоративную. Что делать, если лампа вышла из строя, очевидно снять плафон вывинтить лампу и вставить новую и так далее, но это стало возможно, когда мы применили принцип инверсии зависимости.

Конструкторы разделили светильник на части, в программировании они называются модулями. Каждую из этих частей снабдили интерфейсами. Для лампы сделали интерфейс — патрон. Для плафона интерфейс — держатели и так далее. Теперь есть возможность извлечь лампу и заменить ее на другую, если у этих ламп интерфейсы совместимые. А что если светильник не разборный. Нам придется его разрушить, или выкинуть и купить новый.

То же происходит и в программном обеспечении. Если мы в самом начале неправильно разделили на модули и не выполнили инверсию зависимостей, при малейших изменениях, нам придется переписывать большой участок кода, и это хорошо, если этот код мы писали сами. Разобраться проще.

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

Следующее понятие которое необходимо разобрать, это собственно зависимости. Что отчего зависит. Вернемся к примеру со светильником. Светильник предназначен, для того, чтобы освещать. Это бизнес-логика. Неважно какой там источник света, факел, свеча или электрическая лампа. Бизнес-логика светильника не меняется.

И это важно само по себе. Бизнес-логика почти всех программ описана очень давно, не думаю, что бизнес-процесс торговли изменился со времен древних шумеров, как бы заказчик не настаивал на своей уникальности.

Поэтому, бизнес-логика является неизменной основой для любого приложения. Дальше идет разделение на модули, которые делятся на меньшие и так далее. К сожалению эта статья не про проектирование, поэтому остановимся на примере светильника.

Светильник делится на основу и фонарь. Далее, каждый из этих модулей делятся на свои части и так далее до не разборных частей.

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

В этом и заключается вся суть принципа инверсии зависимостей. Более стабильные части программы не могут быть зависимыми от менее стабильных. Светильник не может быть в зависимости от винтика.

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

enum Classifier { BREAD, BISCUITS, CROISSANTS }; class Product { private: std::string name; // Наименование товара Classifier category; // Классификатор товара float weight; // Вес нетто double price; // Цена public: Product(std::string name, Classifier category, float weight, double price): name(name), category(category), weight(weight), price(price) {} Classifier getClassifier() { return category; } float getWeight() { return weight; } double getPrice() { return price; } }; int main() { std::list<Product> products{ {"Хлеб", Classifier::BREAD, 0.9f, 100}, {"Другой хлеб", Classifier::BREAD, 1.0f, 100}, {"Печенье", Classifier::BISCUITS, 1.0f, 200} }; Product findWhat{ "", Classifier::BREAD, 1.0f, 100 }; auto result = std::ranges::find_if(products, [&findWhat](Product prd){ return findWhat.getClassifier() == prd.getClassifier() and findWhat.getWeight() == prd.getWeight() and findWhat.getPrice() == prd.getPrice(); } ); if (result != products.end()) { std::cout << "Test passed!!!" << std::endl; } }

Пример делится на три части.

  • Метод std::ranges::find_if — это бизнес-логика.

  • std::list<Product> products — это хранилище данных.

  • Product findWhat — это шаблон поиска.

Суть программы незамысловата. В хранилище данных, по шаблону, нам необходимо найти товар. Условия поиска, то есть сам алгоритм поиска прописан в lambda выражении.

Диаграмма классов<br />
Диаграмма классов

Как видно из кода, бизнес-логика стала зависимой от товара. То есть, если нам понадобится изменить товар или условия поиска товара, понадобится менять и бизнес-логику (lambda выражение).

Можно пойти другим путем. Перенести условия в сам класс товара. Реализовав условия, через операторы == или (). Но это еще более тупиковый подход. Во первых, даже для нашего примера с ограничением в три поля понадобится 8 вариантов условий равно/не равно для класса товар, а если полей поиска будет больше или они будут с другими логическими выражениями.

enum Classifier { BREAD, BISCUITS, CROISSANTS }; class Product { private: std::string name; // Наименование товара Classifier category; // Классификатор товара float weight; // Вес нетто double price; // Цена public: Product(std::string name, Classifier category, float weight, double price): name(name), category(category), weight(weight), price(price) {} inline bool operator== (const Product &obj) const { return this->category == obj.category and this->weight == obj.weight and this->price == obj.price; } inline bool operator()(const Product& obj) const { return this->category == obj.category and this->weight == obj.weight and this->price == obj.price; } };

Второй недостаток, класс товар начинает нарушать правило S - принцип единственной ответственности (single responsibility principle) SOLID. Товар отвечает за хранение данных и условия их обработки.

Становится понятным, что условия, назовем их алгоритмами обработки данных, необходимо отделить как от данных, так и от бизнес-логики.

Диаграмма зависимостей<br />
Диаграмма зависимостей

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

class Product {...}; class Comparer { public: bool operator()(Product obj) {...} }; int main() { std::list<Product> list{ ... }; Comparer compare; std::ranges::find_if(list, compare); }

Что бы объяснить проблему давайте возьмем пример из реальной жизни. У вас есть смартфон определенной марки, это ваша бизнес-логика. К нему есть окружение, зарядка, наушники, чехол и т. д. Будем считать их алгоритмами. Если следовать нашей схеме, когда бизнес-логика зависит от алгоритмов, это все равно, что если бы при выходе из строя зарядки или наушников, вам пришлось бы покупать новый смартфон.

Давайте поменяем направление зависимости, пусть алгоритмы не зависят от бизнес-логики.

Диаграмма зависимостей<br />
Диаграмма зависимостей

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

class IComparer { public: virtual bool equal(const Product& first, const Product& second) const = 0; };

Но наша имитация бизнес-логики на основании алгоритма std::ranges::find_if, в качестве параметра принимает унарный предикат, а нам необходимо два значения для сравнения. Как всегда, в сложных и не очевидных случаях, ответ необходимо искать в паттернах проектирования. В данном случае, более всего подходит паттерн «Заместитель» (proxy).

class Comparison { private: Product product; std::shared_ptr<IComparer> predicate; public: Comparison(const Product& product, std::shared_ptr<IComparer> predicate) { this->product = product; this->predicate = predicate; } inline bool operator()(const Product& obj) const { return predicate->equal(this->product, obj); } };

Как видно из кода, объект принимает значение для сравнения и ссылку на алгоритм сравнения в виде интерфейса IComparer. В результате таких преобразований у нас есть заместитель имитирующий унарный предикат, который можно подставить в нашу бизнес-логику основанную на std::ranges::find_if.

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

Чистый код: Инверсия зависимостей (DIP)

В результате получается вот такой код.

class Comparison { private: std::shared_ptr<Product> product; std::shared_ptr<IComparer> predicate; public: Comparison(const std::shared_ptr<Product> product, std::shared_ptr<IComparer> predicate) { this->product = product; this->predicate = predicate; } inline bool operator()(const std::shared_ptr<Product>& obj) const { return predicate->equal(*this->product, *obj); } };

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

enum Classifier { NONE, BREAD, BISCUITS, CROISSANTS }; class Product { private: std::string name; // Наименование товара Classifier category; // Классификатор товара float weight; // Вес нетто double price; // Цена public: Product() : category(Classifier::NONE), weight(0), price(0) {} Product(std::string name, Classifier category, float weight, double price) : name(name), category(category), weight(weight), price(price) {} Classifier getCategory() const { return category; } float getWeight() const { return weight; } double getPrice() const { return price; } }; class Manufacturer {}; class SpecificProduct : public Product { private: Manufacturer manufactured; // Производитель public: SpecificProduct(std::string name, Classifier category, float weight, double price, Manufacturer manufactured) : Product(name, category, weight, price) { this->manufactured = manufactured; } Manufacturer getManufactured() const { return manufactured; } }; class IComparer { public: virtual bool equal(const Product& first, const Product& second) const = 0; }; class ClearMatch : public IComparer { public: virtual bool equal(const Product& first, const Product& second) const { return first.getCategory() == second.getCategory() and first.getWeight() == second.getWeight() and first.getPrice() == second.getPrice(); } }; class FuzzyMatch : public IComparer { public: virtual bool equal(const Product& first, const Product& second) const { return first.getCategory() == second.getCategory() and first.getWeight() == second.getWeight() and (first.getPrice() < 120 or first.getPrice() > 80); } }; class Comparison { private: std::shared_ptr<Product> product; std::shared_ptr<IComparer> predicate; public: Comparison(const std::shared_ptr<Product> product, std::shared_ptr<IComparer> predicate) { this->product = product; this->predicate = predicate; } inline bool operator()(const std::shared_ptr<Product>& obj) const { return predicate->equal(*this->product, *obj); } }; int main() { std::list<std::shared_ptr<Product>> products{ std::make_shared<SpecificProduct>("Хлеб", Classifier::BREAD, 0.9f, 100, Manufacturer()), std::make_shared<SpecificProduct>("Другой хлеб", Classifier::BREAD, 1.0f, 100, Manufacturer()), std::make_shared<SpecificProduct>("Печенье", Classifier::BISCUITS, 1.0f, 200, Manufacturer()) }; std::shared_ptr<Product> findWhat = std::make_shared<SpecificProduct>("", Classifier::BREAD, 1.0f, 100, Manufacturer()); Comparison compare(findWhat, std::make_shared<ClearMatch>()); auto result = std::ranges::find_if(products, compare); if (result != products.end()) { std::cout << "Test passed!!!" << std::endl; } }
11
1 комментарий

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

Ответить