Detailed Inversion of Control and Dependency Injection

foreword

Some time ago, I read the Spring source code, and many concepts were not very clear, so I read some information and gained a lot. Here is a summary of the article Inversion of Control Containers and the Dependency Injection pattern .

At present, the lightweight container of java is very popular, which can assemble the components of different projects and then become an independent application. The bottom layers of these containers all use the same mode to assemble components, that is, "IOC (Inversion of Control/Inversion of Control)", more specifically called "DI (Dependency Injection/Dependency Injection)". The following will mainly compare the differences between IOC/DI and Service Locator (Service Locator), and of course they also have a common role: to separate the configuration and use of components .

key code

There is a very useful movie query component.

 /**
 * 查询电影列表接口:findAll方法用于查询电影列表。
 */
public interface MovieFinder
{
    
    
    List findAll();
}
 
 /**
 * 查询电影列表实现类:用于适配不同的存储方式:txt、xml文件、数据库等。
 * 下面省略get set 构造方法。
 */
class MovieFinderImpl implements MovieFinder{
    
      
    public MovieFinderImpl(String filename)
    {
    
    
        this.filename = filename;
    }
    public List findAll(){
    
    
		//具体实现
	}
}

/**
* moviesDirectedBy可以根据电影导演名从列表中筛选电影。
* 控制反转的目的是让查询和存储解耦,下面省略get set 构造方法。
*/
class MovieLister{
    
      
  private MovieFinder finder;
  
  public MovieLister() {
    
    
    finder = new MovieFinderImpl("movies1.txt");
  }

  public Movie[] moviesDirectedBy(String arg) {
    
    
      List allMovies = finder.findAll();
      for (Iterator it = allMovies.iterator(); it.hasNext();) {
    
    
          Movie movie = (Movie) it.next();
          if (!movie.getDirector().equals(arg)) it.remove();
      }
      return (Movie[]) allMovies.toArray(new Movie[allMovies.size()]);
  }
}

If this component is for personal use, there is no problem at all. But if other people find it easy to use, and their movie list storage method is different from mine, they have to provide a new implementation class of MovieFinder. So in the end, this component will eventually have multiple implementation classes of MovieFinder, and we hope that the MovieLister class can work with any implementation class of MovieFinder, and allow specific implementation classes to be inserted at runtime, and the insertion action is completely out of the control of the plug-in author. The question here is: how to design this connection process so that the MovieLister class can work with its instance without knowing the details of the implementation class . The production environment and the test environment may choose different deployment schemes, and may also choose different interface implementation classes. These emerging lightweight Rongri is to help us assemble these plug-ins into an application. This is one of the main problems with this new breed of lightweight containers, and it's commonly achieved using inversion of control.

inversion of control

So the question is: what control do they invert? Let's compare two examples:

1 Common way:
the early user interface (console) is completely controlled by the application program, you design a series of commands in advance, such as input height, input weight, etc., the application program outputs prompt information one by one, and retrieves the user's response, and finally Show results.

2 Inversion of control:
In the graphical user interface environment, the UI framework will be responsible for executing a main loop, and your application only needs to provide event handlers for each area of ​​the screen.

It can be seen that under the graphical user interface, the main control right of the program is moved from the program to the frame, that is, the control right has been reversed. For a lightweight container like Spring, his control refers to determining the concrete implementation of the interface. In the above demo, the MovieLister class is responsible for determining the specific implementation of MovieFinder, and then MovieFinder is no longer a plug-in, because it is not inserted into the application at runtime. These lightweight containers use a more flexible approach. As long as the plug-in follows certain rules, an independent assembly module can inject the specific implementation of the plug-in into the application. This pattern was later called "Dependency Injection" (Dependency Injection). It should be noted that in order to eliminate the dependencies of programs and plug-in implementations, ServiceLocater can also be used in addition to DI.

DI way

The idea of ​​DI is simply that there are independent classes and assembly classes. The assembly class is used to select the appropriate interface implementation and fill in the corresponding fields, as shown in the figure below (referenced figure): The following will use Spring as an example of three types of dependency
insert image description here
injection way, the file directory is as shown in the figure below.
insert image description here

constructor injection

xml file

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN"
        "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
    <bean id="movieLister" class="MovieLister">
        <constructor-arg ref="movieFinder"/>   写法不唯一,这里不展开了
    </bean>
</beans>

test

public class Application {
    
    
    public static void main(String[] args){
    
    
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-config.xml");

        MovieFinderImpl finder = applicationContext.getBean("movieFinder",MovieFinderImpl.class);
        System.out.println(finder.toString());

    }
}

result
insert image description here

set injection

xml file

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN"
        "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
    <bean id="movieFinder" class="MovieFinderImpl">
        <property name="fileName" value="电影名单.txt"></property>    这里是set方法注入
    </bean>
    <bean id="movieLister" class="MovieLister">
        <constructor-arg ref="movieFinder"/>
    </bean>
</beans>

test

public class Application {
    
    
    public static void main(String[] args){
    
    
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-config.xml");

        MovieLister lister = applicationContext.getBean("movieLister",MovieLister.class);
        System.out.println(lister.toString());

    }
}

result
insert image description here

interface injection

I checked a lot of blogs and saw that from the use of injection methods, interface injection is a method that is not advocated now, because it forces the injected object to implement unnecessary interfaces, which is intrusive. **In order to demonstrate, we need to add an interface in the sample code IMovieListerand let MovieLister implement it

public interface IMovieLister {
    
    
    abstract MovieFinder createMovieFinder();
}
/**
* 注意这里改成了抽象类。构造函数,get,set,toString方法省略
*/
public abstract class MovieLister implements IMovieLister {
    
    

    private MovieFinder finder;
    
    public Movie[] moviesDirectedBy(String arg) {
    
    
        List allMovies = finder.findAll();
        for (Iterator it = allMovies.iterator(); it.hasNext(); ) {
    
    
            Movie movie = (Movie) it.next();
            if (!movie.getDirector().equals(arg)) it.remove();
        }
        return (Movie[]) allMovies.toArray(new Movie[allMovies.size()]);
    }
    
    @Override
    public abstract MovieFinder createMovieFinder();
}

configuration file

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN"
        "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
    <bean id="movieFinder" class="MovieFinderImpl">
        <property name="fileName" value="电影名单.txt"></property>
    </bean>
    <bean id="movieLister" class="MovieLister">
        <lookup-method name="createMovieFinder" bean="movieFinder"></lookup-method>
        <constructor-arg ref="movieFinder"/>
    </bean>
</beans>

test code

public class Application {
    
    
    public static void main(String[] args){
    
    
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-config.xml");
        MovieLister lister = applicationContext.getBean("movieLister",MovieLister.class);
        System.out.println("接口注入和构造方法注入比较: "+ (lister.createMovieFinder()==lister.getFinder()) );
        System.out.println(lister.toString());
    }
}


As a result, it can be seen that the same bean is injected in the two ways.
insert image description here

Service Locator

Dependency injection solves the dependency of MovieLister on the MovieFinder implementation class, so that the author of MovieLister can share the tools with others, and users can complete the MovieFinder implementation class according to their own conditions. There is more than one way to break this dependency, and ServiceLocater can do it too.

The basic idea of ​​Service Locator is to have a Service Locator who knows how to obtain all the services that an application may need. Of course, this just shifts the problem, we still need to put the Service Locator into the MovieLister, the dependencies are as follows:
insert image description here

simple example


public class MovieLister {
    
    
    private MovieFinder finder = ServiceLocator.MovieFinder();  //从ServiceLocator获取实现类

    public Movie[] moviesDirectedBy(String arg) {
    
    
        List allMovies = finder.findAll();
        for (Iterator it = allMovies.iterator(); it.hasNext(); ) {
    
    
            Movie movie = (Movie) it.next();
            if (!movie.getDirector().equals(arg)) it.remove();
        }
        return (Movie[]) allMovies.toArray(new Movie[allMovies.size()]);
    }

    @Override
    public String toString() {
    
    
        return "MovieLister{" +
                "finder=" + finder +
                '}';
    }
}
public class ServiceLocator {
    
    
    private static ServiceLocator soleInstance;  
    private MovieFinder movieFinder;

    public static MovieFinder MovieFinder() {
    
      //获取接口实例
        return soleInstance.getMovieFinder();
    }

    public MovieFinder getMovieFinder() {
    
       
        return movieFinder;
    }

    public static void load(ServiceLocator serviceLocator) {
    
      
        soleInstance = serviceLocator;
    }

    public ServiceLocator(MovieFinder movieFinder) {
    
       
        this.movieFinder = movieFinder;
    }
}

test code

public class Application {
    
    
    public static void main(String[] args){
    
    
        configureServiceLocator(); //当然也可以用配置文件读取配置
        MovieLister lister = new MovieLister();
        System.out.println(lister);
    }

    private static void configureServiceLocator() {
    
    
        ServiceLocator.load(new ServiceLocator(new MovieFinderImpl("movies1.txt")));
    }
}

Running results
insert image description here
For more complex scenarios, we can add a subclass of ServiceLocator; we can call the method of the instance instead of directly obtaining the attributes in the instance; we can implement the process-level ServiceLocator through the process-level storage.

Independent interface mode

In the above, we only need the MovieFinder method of ServiceLocator, but ServiceLocator is directly introduced, and we can isolate it by defining the interface. The specific code is as follows:

define interface

public interface MovieFinderLocator {
    
    
    MovieFinder MovieFinder();
}

interface implementation

public class ServiceLocator implements MovieFinderLocator...
    private static ServiceLocator soleInstance;
    private MovieFinder movieFinder;
    
    public  MovieFinder MovieFinder() {
    
    
        return soleInstance.getMovieFinder();
    }
    public static MovieFinderLocator locator() {
    
    
        return soleInstance;
    }
    public static void load(ServiceLocator serviceLocator) {
    
    
        soleInstance = serviceLocator;
    }

assignment

public class MovieLister...
    private MovieFinderLocator locator = ServiceLocator.locator();
    private MovieFinder finder = locator.MovieFinder();

code testing

public class Application {
    
    
    public static void main(String[] args){
    
    
        configureServiceLocator();
        MovieLister lister = new MovieLister();
        System.out.println(lister);
    }

    private static void configureServiceLocator() {
    
    
        ServiceLocator.load(new ServiceLocator(new MovieFinderImpl("movies1.txt")));
    }
}

operation result
insert image description here

Dynamic ServiceLocator

In the above example, every service you need is statically defined. Of course, we can also use HashMap to dynamically access the Service.

class ServiceLocator...
  private static ServiceLocator soleInstance;
  private Map services = new HashMap();
  
  public static Object getService(String key){
    
    
      return soleInstance.services.get(key);
  }
  public void loadService (String key, Object service) {
    
    
      services.put(key, service);
  }

configuration

  private void configure() {
    
    
      ServiceLocator locator = new ServiceLocator();
      locator.loadService("MovieFinder", new MovieFinderImlp("movies1.txt"));
      ServiceLocator.load(locator);
  }

Obtain

class MovieLister...
  MovieFinder finder = (MovieFinder) ServiceLocator.getService("MovieFinder");

mode selection

AT / ServiceLocator

Both of these patterns provide basic decoupling capabilities, so that the application code does not depend on the specific implementation of the service interface. The most important difference between the two is how the implementation is presented to the application code. When using the Service Locator mode, the application code directly requests the implementation of the service from the ServiceLocator; when using the Dependency Injection mode, the application code does not issue an explicit request, and the implementation of the service is injected into the application, resulting in an inversion of control .

A common reason people tend to use the DI pattern is that it simplifies testing. The key here is: for testing purposes, you must be able to easily switch between the real service implementation and the fake component for testing. However, if you consider it from this perspective alone, there is not much difference between the Dependency Injection mode and the Service Locator mode.

In the previous example, the author wants to hand over the MovieLister class to friends. In this case, it is very good to use the service locator: friends only need to configure the locator to provide appropriate service implementations . . In this case, I don't see any appeal in the inversion of control provided by the Dependency Injection pattern.

However, if you regard MovieLister as a component and provide it to applications written by others, the situation is different. The author cannot predict what kind of service locator API users will use, and each user may have own service locators and are not compatible with each other. One solution is to provide a separate interface for each service, and the user can write an adapter to make my interface work with their service locator. But even so, I still need to find the interface I specified in the service locator. And once the adapter is used, the simplicity provided by ServiceLocator is greatly weakened.

So, the main question is whether the author of the code wants the component he wrote to be used in another application outside his control. If the answer is yes, then he should not make any assumptions about the service locator - even the smallest assumption will cause trouble for the user.

Constructor injection/set method injection

As mentioned above, interface injection is relatively aggressive, and its advantages over the Service Locator mode are not so obvious. Therefore, we generally choose constructor injection or set method injection. The following two methods will be carefully compared.

Another benefit of constructor initialization: you can hide any immutable field - as long as you don't provide it with a set method. If a field should not be changed, it is clear that there is no set method for the field. If the initialization is done through the set method, the exposed setter method is likely to cause some problems.

  1. If there are too many parameters, the constructor will look messy. If there is more than one way to construct a legal object, it is difficult to describe this information through the constructor, because the constructors can only be distinguished by the number and type of parameters (Factory Method mode can solve it).

  2. Constructor injection can also cause some trouble if the parameter to be passed is a simple type like a string. When using set value method injection, you can specify the purpose of the parameter in the name of each set value method; while using constructor injection, you can only rely on the position of the parameter to determine the role of each parameter, and remember the parameter Getting it right is obviously much more difficult.

  3. Things can get especially nasty if objects have multiple constructors and there are inheritance relationships between objects. In order for everything to initialize properly, you have to forward calls to the subclass's constructor to the superclass's constructor, which then handles its own arguments. This may cause further expansion of the size of the constructor.

Despite these pitfalls, it is still recommended that you consider constructor injection first. However, once you face the problems mentioned above, you should be prepared to switch to set method injection.

code/config file

Sometimes it is simpler to implement the assembly directly in the program code. For example, a simple application does not have many changes in deployment. At this time, it is much clearer to configure with a few lines of code than XML files.

Sometimes the assembly of the application is very complex, involving a large number of conditional steps. Once the configuration logic in the programming language starts to become complex, you should use an appropriate language to describe the configuration information to make the program logic clearer. Then, you can write a constructor class to do the assembly work. If there is more than one use case for a constructor, you can provide multiple constructor classes and choose between them through a simple configuration file.

Summarize

The popular lightweight containers all use a common pattern to assemble the services required by the application, called Dependency Injection, which can effectively replace the Service Locator pattern. When developing applications, both have their own advantages and disadvantages, and the behavior of the Service Locator mode is more intuitive. However, if the components you develop are going to be used by multiple applications, then the Dependency Injection mode will be a better choice.

If you decide to use the Dependency Injection mode, it is recommended that you first consider constructor injection; if you encounter some specific problems, then switch to setter method injection. If you want to choose a container and develop on top of it, I suggest you choose a container that supports both injection methods.

Learning Resources

Inversion of Control Containers and the Dependency Injection pattern
Follow me to learn Sprig
Spring's dependency injection (interface injection)

Guess you like

Origin blog.csdn.net/weixin_44532671/article/details/123424858