Dependency Inversion (DIP)

Dependency inversion is a strategy of depending on interfaces or abstract functions and classes rather than concrete functions and classes. Simply put, when components of our system have dependencies, we do not want to directly inject the dependency of one component into another. Instead, we should use some level of abstraction between them.

Here is such a complex definition. Where, “Simply speaking”, only confuses more than explains, although it conveys everything correctly. This principle is perhaps the most difficult to explain, although its essence is obvious.

I will start the explanation with a household example. For the lamp to work, lighting requires a wire, and of course, electricity. It is logical to build this lamp into a light fixture, which will play a dual role, protective and decorative. What to do if the lamp is out of order, obviously remove the lampshade, unscrew the lamp and insert a new one, etc., but this became possible when we applied the principle of inversion of dependence.

The designers divided the lamp into parts, in programming they are called modules. Each of these parts was equipped with interfaces. For the lamp, they made an interface – a socket. For the lampshade, an interface – holders, and so on. Now it is possible to remove the lamp and replace it with another one, if these lamps have compatible interfaces. But what if the lamp is not collapsible? We will have to destroy it, or throw it away and buy a new one.

The same thing happens in software. If we didn't modularize correctly at the very beginning and didn't perform dependency inversion, then with the slightest change we'll have to rewrite a large section of code, and that's good if we wrote that code ourselves. It's easier to figure out.

Moreover, all those verbal spears broken in articles and comments on the topic of refactoring are broken in vain. Correct division into modules and dependency inversion allow to reduce refactoring to replacing one module with another.

The next concept that needs to be analyzed is dependencies. What depends on what. Let's go back to the example with the lamp. The lamp is designed to illuminate. This is business logic. It doesn't matter what kind of light source it is, a torch, a candle, or an electric lamp. The business logic of the lamp does not change.

And this is important in itself. The business logic of almost all programs has been described for a very long time, I don’t think that the business process of trade has changed since the times of the ancient Sumerians, no matter how much the customer insists on its uniqueness.

Therefore, business logic is the constant basis for any application. Then comes the division into modules, which are divided into smaller ones, and so on. Unfortunately, this article is not about design, so we will focus on the example of a lamp.

The lamp is divided into a base and a lantern. Then, each of these modules is divided into its parts and so on until the non-separable parts.

That is, the result is a tree-like structure of the program, where more stable modules consist of less stable ones.

This is the whole point of the dependency inversion principle. More stable parts of a program cannot be dependent on less stable ones. A lamp cannot be dependent on a screw.

To illustrate, let's take a very simple example and perform dependency inversion on it.

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;
    }
}

The example is divided into three parts.

  • The std::ranges::find_if method is business logic.

  • std::list products is a data store.

  • Product findWhat is a search template.

The essence of the program is simple. In the data warehouse, according to the template, we need to find a product. The search conditions, that is, the search algorithm itself, is written in the lambda expression.

Class diagram

Class diagram

As you can see from the code, the business logic has become dependent on the product. That is, if we need to change the product or the product search conditions, we will also need to change the business logic (lambda expression).

You can go another way. Move the conditions to the product class itself. Implement the conditions through the operators == or (). But this is an even more dead-end approach. Firstly, even for our example with a limitation of three fields, you will need 8 equal/not equal conditions for the product class, and if there are more search fields or they have other logical expressions.

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;
    }
};

The second drawback is that the product class begins to violate rule S – the single responsibility principle of SOLID. The product is responsible for storing data and the conditions for its processing.

It becomes clear that conditions, let's call them data processing algorithms, need to be separated from both data and business logic.

Dependency diagram

Dependency diagram

It's a little better, now we can change the conditions independently of the product, but the business logic still depends on the conditions. In reality, the business logic will depend on both the data and the algorithms.

class Product {...};

class Comparer {
public:
    bool operator()(Product obj) {...}
};

int main() {
    std::list<Product> list{ ... };
    Comparer compare;
    std::ranges::find_if(list, compare);
}

To explain the problem, let's take a real-life example. You have a smartphone of a certain brand, this is your business logic. It has an environment, a charger, headphones, a case, etc. Let's consider them algorithms. If we follow our scheme, when business logic depends on algorithms, it's the same as if if the charger or headphones break, you had to buy a new smartphone.

Let's change the direction of dependence, let the algorithms not depend on business logic.

Dependency diagram

Dependency diagram

To perform the inversion, it is necessary to replace all references to the class with algorithms in the business logic with an interface that will be the basis for creating specific implementations.

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

But our imitation of business logic based on the std::ranges::find_if algorithm takes a unary predicate as a parameter, and we need two values ​​for comparison. As always, in complex and non-obvious cases, the answer must be sought in design patterns. In this case, the “Proxy” pattern is most suitable.

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);
    }
};

As you can see from the code, the object takes a value for comparison and a reference to the comparison algorithm in the form of the IComparer interface. As a result of such transformations, we have a proxy simulating a unary predicate, which can be substituted into our business logic based on std::ranges::find_if.

Now it is necessary to perform dependency inversion for working with data. Everything is done exactly the same here. An abstraction is added on the basis of which a class with data is created and in places where the class with data is called directly, a reference to the abstraction is substituted.

Dependency diagram

Dependency diagram

The result is the following code.

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);
    }
};

All conditions are met, we can easily expand the capabilities of data storage and processing algorithms without changing the business logic.

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;
    }
}

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *