Planning Motivation Control

SOLID design principles. Dependency Inversion Principle. A critical look at the principle of inversion of dependencies The Dependency inversion principle is implemented using

14 replies

It basically says:

  • Abstractions should never depend on details. The details should depend on the abstraction.

As for why this is important, in one word: changes are risky, and depending on the concept, not the implementation, you reduce the need for change in call sites.

DIP effectively reduces concatenation between different parts of the code. The idea is that while there are many ways to implement, say, a logger, the way you use it should be relatively stable over time. If you can extract an interface that represents the concept of logging, that interface should be much more stable over time than its implementation, and call sites should be much less susceptible to the changes you might make while maintaining or extending this logging mechanism. ...

Because the implementation depends on the interface, you can choose at runtime which implementation is best for your particular environment. Depending on the case, this can also be interesting.

The books Agile Software Development, Principles, Patterns and Practices and Agile Principles, Patterns and Practices in C # are the best resources for fully understanding the original goals and motivations behind the Dependency Inversion Principle. The Dependency Inversion Principle is also a good resource, but because it is a condensed version of the draft that ended up in the previously mentioned books, it leaves some important discussion about the concept of package ownership and interfaces that are key to differences of this principle from more general advice"program for an interface, not an implementation", which can be found in Design Patterns (Gamma, et al.).

To summarize, the Dependency Inversion Principle is primarily aimed at the change traditional direction of dependencies on components "more high level"to the lower-level components" so that the "lower-level" components depend on the interfaces, owned by higher-level components, (Note: A “higher-level” component here refers to a component requiring external dependencies / services, not necessarily its conceptual position in a layered architecture.) decreases as much as she shifts from components that are theoretically less valuable to components that are theoretically more valuable.

This is accomplished by developing components whose external dependencies are expressed as an interface for which the consumer of the component must provide an implementation. In other words, certain interfaces express what the component needs, not how you use the component (for example, "INeedSomething", not "IDoSomething").

What the Dependency Inversion Principle does not reference is a simple practice of abstracting dependencies with interfaces (e.g. MyService ->). While this decouples the component from the specific implementation detail of the dependency, it does not invert the relationship between consumer and dependency (e.g. ⇐ Logger.

The importance of the Dependency Inversion Principle can be boiled down to a single goal - the ability to reuse software components that rely on external dependencies for a portion of them. functionality(registration, verification, etc.)

Within this overall goal reuse we can distinguish two subtypes of reuse:

    Using a software component in multiple applications with dependency implementations (for example, you've developed a DI container and want to provide logging, but don't want to link your container to a specific logger, so everyone using your container should also use the log library of your choice) ...

    Using software components in an evolving context (for example, you have developed business logic components that remain the same across versions of your application where implementation details evolve).

In the first case of reusing components across multiple applications, for example with an infrastructure library, the goal is to provide consumers with the underlying infrastructure without tying your consumers to the dependencies of your own library, since getting dependencies from such dependencies requires consumers to also require the same dependencies ... This can be problematic when consumers of your library decide to use a different library for the same infrastructure needs (like NLog and log4net), or if they decide to use a later version of the required library that is not backward compatible with the version your library requires.

In the second case of reusing business logic components (ie "Higher Level Components"), the goal is to isolate the main scope application implementation from the changing needs of your implementation details (eg change / update persistent libraries, exchange libraries messages). encryption strategies, etc.). Ideally, changing the implementation details of the application should not break the components that encapsulate the business logic of the application.

Note. Some may object to describing this second case as actual reuse, believing that components such as business logic components used in a single evolving application represent only one use. The idea here, however, is that each change in the implementation details of the application reflects a new context and therefore a different use case, although the end goals can be distinguished as isolation and portability.

While following the Dependency Inversion Principle in the latter case may be of some benefit, it should be noted that its relevance in modern languages ​​such as Java and C # has been greatly reduced, perhaps to the point where it is irrelevant. As discussed earlier, DIP involves the complete separation of implementation details into separate packages. In the case of an evolving application, however, simply using the interfaces defined in terms of the business domain will protect against the need to modify higher-level components due to the changing needs of the implementation detail components, even if the implementation details end up in the same package. ... This part of the principle reflects aspects that were relevant to the language at the time of its codification (for example, C ++) that are not relevant to newer languages. However, the importance of the Dependency Inversion Principle is primarily related to the development of reusable software components / libraries.

A more detailed discussion of this principle, as it deals with simple use of interfaces, dependency injection, and the split interface pattern, can be found.

When we develop software applications, we can consider low-level classes, classes that implement basic and primary operations (disk access, network protocols, and ...) and high-level classes that encapsulate complex logic (business flows, ... ).

The latter rely on low-level classes. The natural way to implement such structures would be to write low-level classes and as soon as we are forced to write complex classes high level. Since high level classes are defined in terms of others, this seems to be the logical way to do it. But this is not a flexible design. What happens if we need to replace a low-level class?

The Dependency Inversion Principle states that:

  • High-level modules should not depend on low-level modules. Both should depend on abstractions.

This principle aims to "invert" the conventional notion that high-level modules in software must depend on the lower level modules. Here, high-level modules own the abstraction (for example, solving interface methods) that are implemented by lower-level modules. Thus, lower-level modules depend on higher-level modules.

Effective use of Dependency Inversion provides flexibility and stability across the entire architecture of your application. This will allow your application to develop more securely and stably.

Traditional layered architecture

Traditionally, the user interface of a layered architecture has depended on the business layer, which in turn has depended on the data access layer.

You must understand a layer, package, or library. Let's see how the code goes.

We would have a library or package for the data access layer.

// DataAccessLayer.dll public class ProductDAO ()

// BusinessLogicLayer.dll using DataAccessLayer; public class ProductBO (private ProductDAO productDAO;)

Layered architecture with dependency inversion

Dependency inversion indicates the following:

High-level modules should not depend on low-level modules. Both should depend on abstractions.

Abstractions should not depend on details. The details should depend on the abstraction.

What are high level and low level modules? Thinking of modules like libraries or packages, high-level modules would be those that traditionally have dependencies and low-level ones that they depend on.

In other words, the high level of the module will be where the action is called and the low level where the action is performed.

From this principle, a reasonable conclusion can be drawn: there should be no dependence between nodules, but there should be dependence on abstraction. But according to the approach we are using, we may misuse the investment dependency, but this is an abstraction.

Imagine we adapt our code like this:

We would have a library or package for a data access layer that defines an abstraction.

// DataAccessLayer.dll public interface IProductDAO public class ProductDAO: IProductDAO ()

And other business logic at the library or package level, which depends on the data access layer.

// BusinessLogicLayer.dll using DataAccessLayer; public class ProductBO (private IProductDAO productDAO;)

Although we depend on abstraction, the relationship between business and data access remains the same.

To get dependency inversion, the persistence interface must be defined in the module or package where the logic or high-level domain resides, not in the low-level module.

First, define what the domain layer is, and the abstraction of its relationship is determined by constancy.

// Domain.dll public interface IProductRepository; using DataAccessLayer; public class ProductBO (private IProductRepository productRepository;)

Once the persistence level is domain dependent, it can now be inverted if the dependency is defined.

// Persistence.dll public class ProductDAO: IProductRepository ()

Deepening the principle

It is important to grasp the concept well, deepening the purpose and benefits. If we stay in mechanics and study a typical repository, we won't be able to determine where we can apply the dependency principle.

But why are we inverting the addiction? What is the main purpose outside concrete examples?

This is usually allows the most stable things, which do not depend on the less stable things, to change more often.

The persistence type is easier to change, either the database or technology to access the same database, than domain logic or actions designed to communicate persistently. Because of this, the relationship is reversed, because it is easier to change persistence if that change occurs. This way we don't have to change the domain. The domain layer is the most stable of all, so it shouldn't depend on anything.

But there is more than just this example of a repository. There are many scenarios that apply this principle, and there are architectures based on this principle.

architecture

There are architectures where dependency inversion is the key to defining it. In all domains this is most important, and it is the abstractions that will specify the communication protocol between the domain and the rest of the packages or libraries.

Clean Architecture

For me, the Dependency Inversion Principle described in the official article

The problem in C ++ is that header files usually contain private field and method declarations. Therefore, if a high-level C ++ module contains a header file for a low-level module, it will depend on the actual implementation details of this module. And this is obviously not very good. But this is not a problem for more modern languages that are commonly used today.

High-level modules are inherently less reusable than low-level modules because the former are usually more application / context specific than the latter. For example, the component that implements the UI screen is at the highest level as well as very (completely?) Application-specific. Attempting to reuse such a component in another application is counterproductive and can only lead to over-development.

Thus, creating a separate abstraction at the same level of component A that depends on component B (that does not depend on A) can only be done if component A is actually useful for reuse across different applications or contexts. If this is not the case, then the DIP application would be bad design.

A clearer way to formulate the Dependency Inversion Principle:

Your modules that encapsulate complex business logic should not directly depend on other modules that encapsulate business logic. Instead, they should only depend on interfaces down to simple data.

I.e., instead of implementing your Logic class like people usually do:

Class Dependency (...) class Logic (private Dependency dep; int doSomething () (// Business logic using dep here))

you should do something like:

Class Dependency (...) interface Data (...) class DataFromDependency implements Data (private Dependency dep; ...) class Logic (int doSomething (Data data) (// compute something with data))

Data and DataFromDependency must live in the same module as Logic, not Dependency.

Why is this?

Good answers and good examples already given by others here.

The point of inversion of dependencies is to make software reusable.

The idea is that instead of two pieces of code relying on each other, they rely on some abstract interface. Then you can reuse any part without the other.

This is usually achieved by inverting a control container (IoC) like Spring in Java. In this model, object properties are configured through XML configuration, not objects exiting and finding their dependencies.

Imagine this pseudocode ...

Public class MyClass (public Service myService = ServiceLocator.service;)

MyClass directly depends on both the Service class and the ServiceLocator class. This is required for both if you want to use it in another application. Now imagine this ...

Public class MyClass (public IService myService;)

MyClass now uses one interface, the IService interface. We would let the IoC container actually set the value of this variable.

Let there be a hotel that asks the food manufacturer for his supplies. The hotel gives the name of the food (say, chicken) to the Food Generator, and the Generator returns the requested food to the hotel. But the hotel doesn't care about the type of food it receives and serves. Thus, the Generator delivers food labeled "Food" to the hotel.

This implementation in JAVA

FactoryClass with a factory method. Food Generator

Public class FoodGenerator (Food food; public Food getFood (String name) (if (name.equals ("fish")) (food = new Fish ();) else if (name.equals ("chicken")) (food = new Chicken ();) else food = null; return food;))

Class Annotation / Interface

Public abstract class Food (// None of the child class will override this method to ensure quality ... public void quality () (String fresh = "This is a fresh" + getName (); String tasty = "This is a tasty "+ getName (); System.out.println (fresh); System.out.println (tasty);) public abstract String getName ();)

Chicken implements Food (Specific Class)

Public class Chicken extends Food (/ * All the food types are required to be fresh and tasty so * They won "t be overriding the super class method" property () "* / public String getName () (return" Chicken ";) )

Fish sells Food (Specific Class)

Public class Fish extends Food (/ * All the food types are required to be fresh and tasty so * They won "t be overriding the super class method" property () "* / public String getName () (return" Fish ";) )

Finally

Hotel

Public class Hotel (public static void main (String args) (// Using a Factory class .... FoodGenerator foodGenerator = new FoodGenerator (); // A factory method to instantiate the foods ... Food food = foodGenerator.getFood ( "chicken"); food.quality ();))

As you could see, the hotel doesn't know if it's chicken or fish. It is only known that it is a food object, i.e. The hotel depends on the class of food.

You might also notice that the Fish and Chicken class implements the Food class and is not directly associated with the hotel. those. chicken and fish also depends on the food grade.

This means that the high-level component (hotel) and the low-level component (fish and chicken) depend on abstraction (food).

This is called dependency inversion.

The Dependency Inversion Principle (DIP) states that

i) High-level modules should not depend on low-level modules. Both should depend on abstractions.

ii) Abstractions should never depend on details. The details should depend on the abstraction.

Public interface ICustomer (string GetCustomerNameById (int id);) public class Customer: ICustomer (// ctor public Customer () () public string GetCustomerNameById (int id) (return "Dummy Customer Name";)) public class CustomerFactory (public static ICustomer GetCustomerData () (return new Customer ();)) public class CustomerBLL (ICustomer _customer; public CustomerBLL () (_customer = CustomerFactory.GetCustomerData ();) public string GetCustomerNameById (int id) (return _customer.GetCustomerNameById (id )) public class Program (static void Main () (CustomerBLL customerBLL = new CustomerBLL (); int customerId = 25; string customerName = customerBLL.GetCustomerNameById (customerId); Console.WriteLine (customerName); Console.ReadKey ();))

Note. A class should depend on abstractions such as an interface or abstract classes, not on concrete details (interface implementation).

to share

, plurality interfaces and dependency inversions. Five flexible principles to guide you every time you write code.

It would be unfair to tell you that any one of the SOLID principles is more important than the other. However, perhaps none of the other principles has such an immediate and profound impact on your code as the Dependency Inversion Principle, or DIP for short. If you find the other principles difficult to understand and apply, then you should start with that and then apply the rest to code that already adheres to the Dependency Inversion Principle.

Definition

A. High-level modules should not depend on lower-level modules. They all have to depend on abstractions.
B. Abstractions should not depend on details. The details should depend on the abstraction.

We should strive to organize our code based on these numbers, and here are some techniques that can help. The maximum length of functions should be no more than four lines (five with the heading), so they can completely fit in our mind. The indentation should be no more than five levels deep. Classes with no more than five methods. In design patterns, the number of classes used typically ranges from five to nine. Our high-level architecture above contains four to five concepts. There are five SOLID principles, each requiring five to nine concepts / modules / classes for examples. The ideal development team size is between five and nine. The ideal number of teams in a company is also between five and nine.

As you can see, the magic number seven, plus or minus two, is everywhere, so why should your code be different.

DISCLAIMER: The author of this article does not intend to undermine the authority or in any way offend such a respected comrade as "uncle" Bob Martin. We are talking here rather about a more careful consideration of the principle of dependency inversion and analysis of the examples used in its description.

Throughout the article, I will provide all the necessary quotes and examples from the above sources. But so that there are no "spoilers" and your opinion remains objective, I would recommend spending 10-15 minutes and familiarizing yourself with the original description of this principle in an article or book.

The dependency inversion principle sounds like this:

A. Top-level modules should not depend on lower-level modules. Both must depend on abstractions.
Q. Abstractions should not depend on details. The details should depend on the abstraction.
Let's start with the first point.

Layering

Onions have layers, cake has layers, cannibals have layers, and software systems have layers too! - Shrek (c)
Any complex system is hierarchical: each layer is built on top of a proven and well-performing lower layer. This allows you to focus on a limited set of concepts at any given time without worrying about how the lower-level layers are implemented.
As a result, we get something like the following diagram:

Figure 1 - "Naive" layering scheme

From the point of view of Bob Martin, such a scheme for dividing the system into layers is naive... The downside to this design is the “insidious feature: the layer Policy depends on changes in all layers on the way to Utility. This dependence is transitive.» .

Hmm ... Quite an unusual statement. If we talk about the .NET platform, then the dependency will be transitive only if the current module "exposes" lower-level modules in its open interface. In other words, if in MechanismLayer there is a public class that takes an instance as an argument StringUtil(from UtilityLayer), then all clients of the level MechanismLayer become addicted to UtilityLayer... Otherwise, there is no transitivity of changes: all changes to the lower level are limited to the current level and do not propagate above.

To understand Bob Martin's idea, you need to remember that for the first time the dependency inversion principle was described back in 1996, and the C ++ language was used as examples. In the original article, the author himself writes that the problem of transitivity is only in languages ​​without a clear separation of the class interface from the implementation... In C ++, the problem of transitive dependencies is really relevant: if the file PolicyLayer. h includes via the "include" directive MechanismLayer. h which in turn includes UtilityLayer. h, then with any change in the header file UtilityLayer. h(even in the "private" section of the classes declared in this file) we will have to recompile and redeploy all clients. However, in C ++, this problem is solved by using the PIml idiom proposed by Herb Sutter and is now also not so relevant.

The solution to this problem from the point of view of Bob Martin is as follows:

“The higher layer declares the abstract interface of the services it needs. The lower layers are then implemented to satisfy these interfaces. Any class located at the top level accesses the layer of the next level below through an abstract interface. Thus, the upper layers are independent of the lower ones. Conversely, the lower layers depend on the abstract service interface, announced at a higher level ... Thus, by reversing dependencies, we created a structure that is at the same time more flexible, durable and mobile.



Figure 2 - Inverted layers

In a way, such a partition is reasonable. So, for example, when using the observer pattern, it is the observable that defines the interface for interacting with outside world, therefore no external changes cannot influence it.

But on the other hand, when it comes to layers, which are usually represented by assemblies (or packages in UML terms), the proposed approach can hardly be called viable. By definition, low-level helper classes are used in a dozen different higher-level modules. Utility Layer will be used not only in Mechanism Layer, but also in Data Access Layer, Transport layer, Some other layer... Should it then implement the interfaces defined in all higher-level modules?

Obviously, such a solution is hardly ideal, especially considering that we are solving a problem that does not exist on many platforms, such as .NET or Java.

Abstraction concept

Many terms are so ingrained into our brains that we stop paying attention to them. For most "object-oriented" programmers, this means that we stop thinking about many hackneyed terms like "abstraction", "polymorphism", "encapsulation". Why think about them, because everything is clear anyway? ;)

However, in order to accurately understand the meaning of the Dependency Inversion Principle and the second part of the definition, we need to return to one of these fundamental concepts. Let's take a look at the definition of the term "abstraction" from Grady Booch's book:

Abstraction highlights the essential characteristics of an object that distinguish it from all other types of objects and, thus, clearly defines its conceptual boundaries from the point of view of the observer.

In other words, abstraction defines the visible behavior of an object, which in programming languages ​​is defined by the object's public (and protected) interface. Very often we model abstractions with interfaces or abstract classes, although from an OOP point of view this is not required.

Let's go back to the definition: Abstractions should not depend on details. The details should depend on the abstraction.

What example arises in my head now, after we have remembered what it is abstraction? When does abstraction start to depend on details? An example of a violation of this principle is the abstract class GZipStream which takes MemoryStream, not an abstract class Stream:

Abstract class GZipStream (// The GZipStream abstraction accepts a specific stream protected GZipStream (MemoryStream memoryStream) ())

Another example of violation of this principle could be an abstract repository class from the data access layer, taking in the constructor PostgreSqlConnection or a connection string for SQL Server, which makes any implementation of such an abstraction tied to a specific implementation. But is that what Bob Martin means? Judging by the examples given in the article or in the book, Bob Martin understands something completely different by the concept of "abstraction".

PrincipleDIPaccording to Martin

To explain his definition, Bob Martin gives the following explanation.

A slightly simplified, but still very powerful interpretation of the DIP principle is expressed by a simple heuristic rule: "You have to depend on abstractions." It states that there should be no dependencies on specific classes; all links in the program must lead to an abstract class or interface.

  • There should be no variables that store references to specific classes.
  • There should be no classes derived from concrete classes.
  • There should be no methods that override a method implemented in one of the base classes.

As an illustration of the violation of the DIP principle in general, and the first "clarifying" point, in particular, the following example is given:

Public class Button (private Lamp lamp; public void Poll () (if (/ * some condition * /) lamp.TurnOn ();))

Now let's remember once again what it is abstraction and answer the question: is there an "abstraction" here that depends on the details? While you are thinking about this or looking with your eyes for the paragraph in which the answer to this question is located, I want to make a small digression.

The code has one interesting feature... With rare exceptions, the code itself cannot be correct or incorrect; a bug or a feature depends on what is expected of it. Even if there is no formal specification (which is the norm), the code is incorrect only if it does not do what is required or assumed of it. It is this principle that underlies contract programming, in which the specification (intent) is expressed directly in the code in the form of preconditions, postconditions and invariants.

Looking at the class Button I can't tell if the design is wrong or not. I can tell for sure that the name of the class does not match its implementation. The class needs to be renamed to LampButton or remove from the class Button field Lamp.

Bob Martin insists that this design is flawed because “the high-level application strategy is not separate from the low-level implementation. Abstractions are not separated from details. In the absence of this separation, the top-level strategy automatically depends on the lower-level modules, and the abstraction automatically depends on the details. "

At first, I do not see in this example"Top-level strategies" and "lower-level modules": from my point of view, classes Button and Lamp are at the same level of abstraction (in any case, I see no arguments to prove otherwise). The fact that the class Button can manage someone does not make it higher-level. Secondly, there is no "detail-dependent abstraction," there is "detail-dependent implementation of abstraction," which is not the same thing at all.

Martin's solution is:



Figure 3 - "Inverting Dependencies"

Is it better this decision? Let's see…

The main benefit of Martin Dependency Inversion is ownership inversion. In the original design, when the class changes Lamp would have to change the class Button... Now the class Button"Owns" the interface ButtonServer, but it cannot change due to changes in the "lower levels", such as Lamp... Quite the opposite: changing the class ButtonServer possible only under the influence of changes in the Button class, which will lead to a change in all inheritors of the class ButonServer!

Last updated: 03/11/2016

Dependency Inversion Principle(Dependency Inversion Principle) is used to create loosely coupled entities that are easy to test, modify, and update. This principle can be formulated as follows:

Top-level modules should not depend on lower-level modules. Both must depend on abstractions.

Abstractions should not depend on details. The details should depend on the abstraction.

To understand the principle, consider the following example:

Class Book (public string Text (get; set;) public ConsolePrinter Printer (get; set;) public void Print () (Printer.Print (Text);)) class ConsolePrinter (public void Print (string text) (Console.WriteLine (text);))

The Book class, which represents a book, uses the ConsolePrinter class for printing. Defined like this, the Book class depends on the ConsolePrinter class. Moreover, we have hard-coded that the book can only be printed to the console using the ConsolePrinter class. Other options, for example, output to a printer, output to a file or using some elements of the graphical interface - all this is excluded in this case. The book printing abstraction is not separated from the details of the ConsolePrinter class. All of this is a violation of the Dependency Inversion Principle.

Now let's try to bring our classes in accordance with the principle of dependency inversion, separating abstractions from the low-level implementation:

Interface IPrinter (void Print (string text);) class Book (public string Text (get; set;) public IPrinter Printer (get; set;) public Book (IPrinter printer) (this.Printer = printer;) public void Print ( ) (Printer.Print (Text);)) class ConsolePrinter: IPrinter (public void Print (string text) (Console.WriteLine ("Print to console");)) class HtmlPrinter: IPrinter (public void Print (string text) ( Console.WriteLine ("Print to html");))

The abstraction of printing the book is now decoupled from concrete implementations. As a result, both the Book class and the ConsolePrinter class depend on the IPrinter abstraction. In addition, we can now also create additional low-level implementations of the IPrinter abstraction and dynamically apply them in the program:

Book book = new Book (new ConsolePrinter ()); book.Print (); book.Printer = new HtmlPrinter (); book.Print ();