In the book Tao nature-object-oriented Practice Guide, we use a dialectical relationship between opposites to illustrate the "template method" model-"positive dependency. dependency inversion "(see section 15th of" tao nature "[Wang yongwu, Wang Yonggang 2004]). This view of putting the "Hollywood" principle and the "Dependency inversion" principle in an equal volume actually comes from the section on the lightweight container PicoContainer homepage:
"One of the famous synonymous principles of Inversion of Control is by Robert C. another nickname of Martin's Dependency Inversion Principle is the Hollywood Principle (Hollywood Principle: Don't call me, let me call you) "[PicoContainer 2004].
After in-depth discussions with netizens on the CSDN Blog, I reorganized these concepts. I found that although these concepts are unified at the macro level of thinking and motivation, there are still many subtle differences at the specific application level. This article uses several simple examples to further analyze the Dependency Inversion Principle, Inversion of Control, and Dependency Injection concepts, it is also a supplement to the content of Tao nature.
Dependency and Coupling)
The help document of Rational Rose defines the "dependency" relationship as follows: "dependency describes the relationship between two model elements, if the dependent model element changes, it will affect another model element. Typically, in the class diagram, dependency indicates that operations of the customer class call operations of the server class ."
Martin Fowler described Coupling as follows in the article cutting Coupling: "If one module of a program requires another module to change at the same time, the two modules are considered coupled ." [Fowller 2001]
From the above definition, we can see that if module A calls the method provided by Module B or accesses some data members in Module B (of course, this is not recommended in object-oriented development ), we think that module A depends on Module B, and module A and Module B are coupled.
So is dependency a good thing or a bad thing for us?
Human Understanding is limited, making it difficult for most people to understand and grasp overly complex systems. The software system is divided into multiple modules, which can effectively control the complexity of the module, so that each module is easy to understand and maintain. However, in this case, information must be exchanged between modules in some way, that is, some coupling relationship must occur. If a module has no association with other modules (even if it is only a potential or implicit dependency), we can almost conclude that this module does not belong to this software system, should be removed from the system. If there is no coupling relationship between all modules, the result is: The whole software is a simple accumulation of multiple unrelated systems. For each system, all functions must be implemented in one module, which means that no module is decomposed.
Therefore, there must be one or more dependencies between modules. Never imagine eliminating all dependencies. However, a strong coupling relationship (for example, a module change may cause one or more other modules to change the dependency relationship at the same time) will cause great harm to the quality of the software system. Especially when the demand changes, the Code maintenance cost will be very high. Therefore, we must try our best to control and eliminate unnecessary coupling, especially the dependency that will lead to uncontrollable changes in other modules. The principles of dependency inversion, control reversal, and dependency injection are constantly generated and developed in the arduous struggle with dependency.
Separation of interfaces and Implementations
Separating interfaces and implementations is the first attempt to control dependencies. Figure 1 shows the first example of Robert C. Martin in dependency inversion [Martin 1996. ReadKeyboard () and WritePrinter () are two functions in the function library. The application calls these two functions cyclically to copy the characters you typed to the printer output.
In order to make the application program independent from the specific implementation of the function library, the C language writes the function definition in a separate header file (function library. h. The advantage of this approach is that, although the application needs to call the function library and rely on the function library, when we want to change the implementation of the function library, we only need to rewrite the implementation code of the function, the application does not need to change. For example, if you change the function library. c file and re-implement the WritePrinter () function to output data to the disk, the function of the program changes as long as the application and function library are re-linked.
The preceding function library can also be implemented in C ++. We usually call this implementation with object-oriented technology the module that provides multiple support classes for applications as a "class library", as shown in figure 2. This method of eliminating the dependency between applications and class libraries through the separation interface and implementation has the following features:
1. The class library called by the application depends on the class library.
2. The separation of interfaces and implementations resolves this dependency to a certain extent, and the specific implementation can change during compilation. However, this solution has very limited functions. For example, a system cannot accommodate multiple implementations, but different implementations cannot change dynamically. It is strange to use the WritePrinter function name to implement the function output to the disk.
3. The class library can be reused separately. However, applications cannot be reused without the class library unless a class library that implements the same interface is provided.
Dependency Inversion (Dependency Inversion Principle)
It can be seen that the method of simple separation interface discussed above has very limited effect on the dissolution of dependencies. Java provides pure interface classes, which do not include any implementation code and can better isolate two modules. Although this pure interface class is not defined in the C ++ language, all member functions are pure virtual functions and abstract classes do not contain any implementation code, which can play a role similar to the Java interface class. To be different from the simple interface mentioned in the previous section, the interface defined based on the Java interface class or the C ++ abstract class is called an abstract interface. The principle of dependency inversion is based on abstract interfaces. Robert Martin describes the Dependency inversion principle [Martin 1996]:
A. Upper-layer modules should not depend on lower-layer modules. They both depend on an abstraction.
B. abstraction cannot depend on visualization, while it depends on abstraction.
To eliminate the dependency between two modules, an abstract interface should be defined between the two modules. The upper-layer module calls the function defined by the abstract interface, and the lower-layer module implements the interface. 3. For the example in the previous section, we can define two abstract classes: Reader and Writer. The Read () and Write () functions are pure virtual functions, the specific KeyboardReader and PrinterWriter classes implement these interfaces. When an application calls the Read () and Write () functions, the implementation of the KeyboardReader and PrinterWriter classes is actually called due to the role of the polymorphism mechanism. Therefore, abstract interfaces isolate the specific classes in applications and class libraries so that there is no direct coupling between them and they can be expanded or reused independently. For example, we can use a similar method to implement the FileReader or DiskWriter class. The application can either input from the keyboard or file as needed or output to the printer or disk, you can even complete Multiple Input and Output tasks at the same time. It can be concluded that this method of resolving the dependency between applications and class libraries through abstract interfaces has the following characteristics:
1. The application calls the abstract interface of the class library and relies on the abstract interface of the class library. The specific implementation class derives from the abstract interface of the class library and also relies on the abstract interface of the class library.
2. the implementation of applications and specific class libraries is completely independent, and there is no direct dependency between them. As long as the interface class is stable, the specific implementation of applications and class libraries can change independently.
3. The class library can be reused independently, and the application can work together with any class library that implements the same abstract interface.
Generally, because the designer of the class library does not know how the application uses the class library, abstract interfaces are mostly summarized by the class library designer according to the typical usage mode they have imagined and retain a certain flexibility, to be used by application developers.
However, there is another situation. Figure 4 is an example [Fowler 2001] used by Martin Fowler in the article "cutting Coupling". The Domain package must use the database package, that is, the Domain package depends on the database package. To isolate the Domain package and database package, you can introduce a Mapper package. If we want the Domain package to be reused multiple times and the Mapper package can change at any time under specific circumstances, we must prevent the Domain package from relying too much on the Mapper package. At this time, the designers of the Domain package can summarize the abstract interfaces (such as Store) they need, and the designers of the Mapper package can implement this abstract interface. In this way, dependencies are completely reversed at the interface level and implementation level.
Inversion of Control)
The dependency between the application and the class library is described above. If we develop a framework system instead of a class library, the dependency will be stronger. So how can we eliminate the dependency between the framework and applications?
Chapter 5th of "tao nature" describes the differences between the framework and class libraries:
"The most important difference between a framework and a class library is that a framework is a semi-finished application, and a class library only contains a series of classes that can be called by applications.
"The Class Library provides users with a series of reusable classes. These classes are designed in line with the object-oriented principles and patterns. You can create instances of these classes, or inherit new Derived classes from these classes, and then call the corresponding functions in the class. In this process, the class library always passively responds to users' call requests.
"The framework implements a basic and executable architecture for a specific purpose. The framework already contains the main process from application startup to running. The steps in the process that cannot be predefined are left to users for implementation. When the program is running, the framework system automatically calls user-implemented functional components. At this time, the framework system is active.
"We can say that the class library is dead, and the framework is live. The application calls the class library to complete specific functions, and the Framework calls the application to implement the entire operation process. The framework is the perfect embodiment of the principle of control inversion ."
A best example of a framework system is a graphical user interface (GUI) system. A simple GUI system developed using a process-oriented design method is shown in Figure 5.
As shown in figure 5, the application calls the CreateWindow () function in the GUI framework to create a window. Here, we can say that the application depends on the GUI framework. However, the GUI framework does not know how to process the window message received by the window. This is only the most clear for the application. Therefore, when the GUI framework needs to send window messages, it must call a specific window function defined by the application (such as MyWindowProc in ). In this case, the GUI framework must depend on the application. This is a typical bidirectional dependency. This two-way dependency has a very serious defect: Because the GUI Framework calls a specific function (MyWindowProc) in the application, the GUI framework cannot exist independently. For a new application, most of the GUI frameworks need to be modified accordingly. Therefore, how to eliminate the dependency between the framework system and applications is the key to implementing the framework system.
Not only object-oriented methods can solve this problem. WIN32 API has long provided us with examples to solve similar problems under the process-oriented design idea. The architecture model of the WIN32 class is shown in Figure 6.
In Figure 6, when an application calls the CreateWindow () function, it must pass a pointer to the message processing function to the GUI framework (for WIN32, we pass this pointer when registering the window class). The GUI framework records this pointer in the window information structure. When a window message needs to be sent, the GUI Framework calls the window function through this pointer. Compared with figure 5, the GUI framework still needs to call the application, but this call changes from a hard-coded function call to a dynamic call that is registered by the application in advance by the called object. Figure 6 shows this dynamic call with a dotted line. It can be seen that this dynamic call relationship has a great advantage: When an application changes, it can change the call target of the framework system on its own, without the need to change the GUI framework. Now, we can say that although there are still calling relationships from the GUI framework to the application, the GUI framework is no longer dependent on the application. This kind of dynamic calling mechanism is also called a callback function ".
In the Object-Oriented field, the alternative of "callback function" is "template method mode", that is, "Hollywood principles (do not call us, let us call you )". An Object-Oriented Implementation of the GUI Framework 7 is shown.
In Figure 7, "GUI framework abstract interface" is an interface provided by the GUI framework system to applications. The motive for abstracting this interface is to eliminate the direct dependency between the application and the GUI framework based on the "Dependency inversion" principle, to minimize the impact of changes in the GUI framework on applications. Window Interface Class is the core of "template method mode. When the application calls the CreateWindow () function, the GUI framework saves the reference of the window in the window linked list. When a Window message needs to be sent, the GUI Framework calls the SendMessage () function of the Window object, which is a non-virtual member function in the Window class. The SendMessage () function calls the WindowProc () virtual function again. Here, the WindowProc () function implemented in the MyWindow class of the application is actually executed. In Figure 7, we can no longer see the direct dependency between the GUI framework and applications. Therefore, the template method fully implements the dynamic call mechanism of the callback function, eliminating the dependency between the framework and the application.
From the above analysis, we can see that the template method mode is the basis of the framework system, and any framework system cannot do without the template method mode. Martin Fowler also said [Folwer 2004], "the authors of several lightweight containers are proud to say to me that these containers are very useful because they implement a 'control invert '. I am deeply confused by the rhetoric: Control reversal is a common feature of the Framework. If only control reversal is used, these lightweight containers are considered to be different, it's like saying, 'my car is different because it has four wheels '. The key to the problem is: what control did they reverse? The first control reversal I encountered was the control right on the user interface. Early user interfaces were completely controlled by applications. You designed a series of commands in advance, such as 'input name' and 'input address'. The application output prompt information one by one, and retrieve the user's response. In the graphic user interface environment, the UI framework is responsible for executing a main loop. Your application only needs to provide event processing functions for various areas of the screen. Here, the main control right of the program is reversed: From the application to the Framework ."
Indeed: as shown in figure 3 and figure 7, when using a common class library, the main loop of the program is located in the application, and the application using the framework system does not include a main loop, only interfaces defined by some frameworks are implemented. The framework system is responsible for implementing the main cycle of system operation and calling applications in template mode when necessary.
That is to say, although "Dependency inversion" and "control inversion" are both effective methods to eliminate module coupling at the design level, both try to make specific and variable modules depend on the basic principles of abstract and stable modules. However, there are differences between the two in terms of context and focus: "Dependency inversion" emphasizes "inversion" of the traditional hierarchical concepts originating from process-oriented design ideas, while "control inversion" emphasizes the reversal of control over program processes; the "Dependency inversion" is more widely used to describe program processes (such as the master-slave and hierarchical relationships of processes ), it can also be used to describe other conceptual design models (such as service components and customer components, core modules and peripheral applications ), "Control inversion" is only applicable to scenarios that describe control of a process (such as control of an algorithm flow or business flow ).
In a sense, we can also regard "control inversion" as a special case of "Dependency inversion. For example, the "control inversion" Mechanism Implemented in the template method is to abstract an interface class between the framework system and the application to describe the prototype of all algorithm steps, the framework system depends on the interface class to define and implement the program process. The application depends on the interface class to provide the implementation of specific algorithm steps, the application's dependency on the framework system is "Inverted" as the dependency between the two on the abstract interface.
In general, the dependency between applications and framework systems has the following characteristics:
1. The application and framework system are actually two-way calls and two-way dependencies.
2. The dependency inversion principle can weaken the dependency between applications and frameworks.
3. "control reversal" and specific template method modes can dissolve the dependency between the framework and the application, which is also the basis of all framework systems.
4. The framework system can be reused independently.
Dependency Injection)
In the previous example, we used the "Dependency inversion" principle to minimize the dependency between the application Copy class and the Read and Write services provided by the class library. However, if you need to implement the Copy () function in the class library, what will happen? Assume that a "service class" is implemented in the class library, and the "service class" provides the Copy () method for application use. When using an application, first create an instance of the "service class" and call the Copy () function. The KeyboardReader and PrinterWriter class instance objects will be created when the "service class" instance is initialized. 8.
As shown in figure 8, although the Reader and Writer interfaces isolate the "service class" and the specific Reader and Writer classes, the coupling between them is minimized. However, when the "service class" creates a specific Reader and Writer object, the "service class" still depends on the specific Reader and Writer objects. in figure 8, the blue dotted line is used to describe the dependency.
In this case, it is critical to instantiate the specific Reader and Writer classes and minimize the dependency of the Service Classes on them. If the service class is in the application, this dependency will not affect us much. However, when the "service class" is located in the class library that needs to be independently released, its code cannot change with the change of the application. This also means that if the "service class" is excessively dependent on the specific Reader and Writer classes, you cannot add new Reader and Writer implementations on your own.
To solve this problem, "dependency injection" is used to cut off the dependency between the "service class" and the specific Reader and Writer classes, and the application injects this dependency. 9.
In Figure 9, the "service class" is not responsible for creating instance objects of the specific Reader and Writer classes, but for creating instances by applications. When an application creates a "service class" instance object, it injects the reference of the specific Reader and Write object into the "service class. In this way, the code in the "service class" is only related to the abstract interface. When the specific implementation code changes, the "service class" will not change. When adding a new implementation, you only need to change the application code to define and use the new Reader and Writer classes, this type of dependency injection is also known as "Constructor injection ".
If an injection interface is abstracted for the Copy class, the application injects dependency through the interface. This injection method is usually called "interface injection ". If a set-value function is provided for the Copy class, the application injects Dependencies by calling the set-value function. This method of dependency injection is called "Set-value injection ". For details about "interface injection" and "Set Value injection", refer to [Martin 2004].
Both PicoContainer and Spring lightweight container frameworks provide corresponding mechanisms to help users implement different "dependency injection ". In addition, they also support defining dependencies in XML files in different ways, and then the application calls the framework to inject dependencies. When the dependencies need to change, you only need to modify the corresponding XML file.
Therefore, the core idea of dependency injection is:
1. Abstract interfaces isolate the dependencies between users and implementations, but creating instance objects for specific implementation classes still results in dependency on specific implementations.
2. Dependency injection can be used to eliminate such creation dependencies. After dependency injection is used, some classes are completely written based on abstract interfaces, which can adapt to the changes to the greatest extent possible.
Conclusion
Separation interfaces and implementations are the first attempts to effectively control dependencies, while pure abstract interfaces better isolate the two modules that depend on each other, the "Dependency inversion" and "control inversion" principles describe the motives for using abstract interfaces to eliminate coupling from different perspectives. The GoF design model is the perfect embodiment of this motion. The creation process of a specific class is another common dependency. In the "dependency injection" mode, you can set the creation process of a specific class to a proper place, this mechanism is similar to the GoF creation mode.
These principles provide good guidance for our practice, but they are not the Bible and may vary on different occasions, we should use it flexibly in the development process based on the possibility of changing requirements.