Home  >  Article  >  Java  >  Detailed explanation of how Java uses three design patterns of IOC inversion of control

Detailed explanation of how Java uses three design patterns of IOC inversion of control

黄舟
黄舟Original
2017-10-18 10:16:561446browse

This article mainly introduces the three design patterns of Java using IOC control inversion in detail. It has certain reference value. Interested friends can refer to it.

For many developers Say, inversion of control (IoC) is a vague concept because they have little or no application in the real world. In the best case, Inversion of Control (IoC) can be thought of as the equivalent of Dependency Injection (DI). In fact, flipping control and dependency injection are considered equivalent only if both sides only reflect flipped dependency management control. Although, dependency injection is actually a well-known form of IoC. However, in fact, IoC is a relatively broader software design paradigm that can be implemented through a variety of patterns. In this article, we will introduce how dependency injection, observer pattern and template method pattern implement inversion of control.

Just like many other design patterns, which are summarized from various usage scenarios, the implementation of IoC is also a similar compromise method suitable for developers:

On the one hand, the design of highly decoupled components and encapsulation of application logic in a single place is a direct and natural way to implement IoC.
On the other hand, the above implementation requires at least one layer of indirection to be built, which may be an over-design in some use cases.
Next, let’s look at a few specific implementations, which will help you understand how to make trade-offs between these properties.

IOC Paradigm Revealed

Inversion of control is a pattern with certain characteristics. Below, a classic IOC example given by Martin Fowler is given, which implements the function of collecting user data from the console.


public static void main(String[] args) {
  while (true) {
    BufferedReader userInputReader = new BufferedReader(
        new InputStreamReader(System.in));
    System.out.println("Please enter some text: ");
    try {
      System.out.println(userInputReader.readLine());
    } catch (IOException e) {
      e.printStackTrace();
    }
  }
}

In this use case, process control is performed in the main method: in the infinite loop call, read user input and output the read content to on the console. The main method completely controls when to read user input and when to output.

Consider a new version of the above program. In this version, user input needs to be received through a text box in the graphical interface. There is also a button with an action listener bound to it. In this case, every time the user clicks the button, the entered text is collected by the listener and printed to the panel.

In this version of the program, it is actually called by the developer for reading and printing user input, under the control of the event listener model (in this case, this is the framework) code. Simply put, the framework will call the developer's code and not the other way around. The framework is actually an extensible structure that provides developers with a set of entry points for injecting custom code snippets.

In this case, control has been effectively reversed.

From a more general perspective, each callable extension point defined by a framework (in the form of an interface implementation, implementation inheritance (also called a subclass)) is a well-defined form of IoC .

Look at the following simple Servlet example:


public class MyServlet extends HttpServlet {
 
  protected void doPost(
      HttpServletRequest request, HttpServletResponse response)
      throws ServletException, IOException {
    // developer implementation here
  }
 
  protected void doGet(
      HttpServletRequest request, HttpServletResponse response)
      throws ServletException, IOException {
    // developer implementation here
  }
 
}

Here, the HttpServlet class (belonging to the framework) fully controls the program element instead of the MyServlet subclass. After being created by the servlet container, when the servlet's GET and POST HTTP requests are received, the code in the doGet() and doPost() methods will be automatically called respectively.

Compared with typical inheritance, where the subclass is the element of the control rather than the base class, in this example the control has been inverted.

In fact, the servlet method is the implementation of the template method pattern, which we will discuss in depth later.

When using frameworks that adhere to the open-closed principle by providing extensible APIs, the role of the developer using the framework ultimately boils down to defining their own set of custom classes, i.e. the developer either implements the framework One or more interfaces provided, either by inheriting from an existing base class. In turn, instances of classes are instantiated directly by the framework, and these instances are called by the framework.

To quote Fowler here: The framework calls the developer, rather than the developer calling the framework. Therefore, IoC is often called the Hollywood principle: don't call us, we'll call you.

How to implement IOC

Regarding this issue, it is obvious that there are several different ways to implement inversion of control. We might as well summarize those common implementation methods.

Injecting dependencies to implement IOC
As mentioned above, injecting dependencies is a way to implement IOC, and it is the most common object-oriented design method. However, think about it: How does injecting dependencies achieve the inversion of control effect?

In order to answer this question, we give the following original example:


public interface UserQueue { 
  void add(User user); 
  void remove(User user);
  User get();
 
}
 
public abstract class AbstractUserQueue implements UserQueue {
  protected LinkedList queue = new LinkedList<>();
 
  @Override
  public void add(User user) {
    queue.addFirst(user);
  }
 
  @Override
  public void remove(User user) {
    queue.remove(user);
  }
 
  @Override
  public abstract User get();
 
}
 
public class UserFifoQueue extends AbstractUserQueue { 
  public User get() {
    return queue.getLast();
  }
 
}
 
public class UserLifoQueue extends AbstractUserQueue {
  public User get() {
    return queue.getFirst();
  }
 
}

The UserQueue interface defines a public API for Store User objects in a queue (for simplicity and clarity, the specific implementation of User is ignored here). AbstractUserQueue provides some public method implementations for subsequent inherited classes. The last UserFifoQueue and UserLifoQueue implement FIFO and LIFO queues respectively.

这是,实现子类多态性的一种有效方式。但是这具体用什么来买我们好处呢?实际上,好处还是蛮多的。

通过创建一个依赖于UserQueue抽象类型(也称为DI术语中的服务)的客户端类,可以在运行时注入不同的实现,无需会重构使用客户端类的代码:


public class UserProcessor { 
  private UserQueue userQueue;
 
  public UserProcessor(UserQueue userQueue) {
    this.userQueue = userQueue;
  }
 
  public void process() {
    // process queued users here
  }
 
}

UserProcessor展示了,注入依赖确实是IOC的一种方式。

我们可以通过一些硬编码方式 如 new 操作,直接在构造函数中实例化在UserProcessor中获取对队列的依赖关系。但是,这是典型的代码硬编程,它引入了客户端类与其依赖关系之间的强耦合,并大大降低了可测性。耳边警钟声声想起啦!不是吗?是的,这样设计真的很挫。

该类在构造函数中声明对抽象类 UserQueue 的依赖。也就是说,依赖关系不再通过 在构造函数中使用 new 操作, 相反,通过外部注入的方式,要么使用依赖注入框架(如CDI和谷歌的Guice),要么使用factory或builders模式。

简而言之,使用DI,客户端类的依赖关系的控制,不再位于这些类中;而是在注入器中进行:


public static void main(String[] args) {
   UserFifoQueue fifoQueue = new UserFifoQueue();
   fifoQueue.add(new User("user1"));
   fifoQueue.add(new User("user2"));
   fifoQueue.add(new User("user3"));
   UserProcessor userProcessor = new UserProcessor(fifoQueue);
   userProcessor.process();
}

上述方式达到了预期效果,而且对UserLifoQueue的注入也简单明了。显而易见,DI确实是实现IOC的一种方式(该例中,DI是实现IOC的一个中间层)。

观察者模式实现IOC

直接通过观察者模式实现IOC,也是一种常见的直观方式。广义上讲,通过观察者实现IOC,与前文提到的通过GUI界面中的action监听器方式类似。但是在使用action监听器情况下,只有在特定的用户事件发生时(点击鼠标,键盘或窗口事件等),才会发生调用。观察者模式通常用于在模型视图的上下文中,跟踪模型对象的状态的变迁。

在一个典型的实现中,一到多个观察者绑定到可观察对象(也称为模式术语中的主题),例如通过调用addObserver方法进行绑定。一旦定义了被观察者和观察者之间的绑定,则被观察者状态的变迁都会触发调用观察者的操作。

为了深入了解这个概念,给出如下例子:


@FunctionalInterface
public interface SubjectObserver {
 
  void update();
 
}

值发生改变时,会触发调用上述这个很简单的观察者。真实情况下,通常会提供功能更丰富的API,如需要保存变化的实例,或者新旧值,但是这些都不需要观察action(行为)模式,所以这里举例尽量简单。

下面,给出一个被观察者类:


public class User {
 
  private String name;
  private List observers = new ArrayList<>();
 
  public User(String name) {
    this.name = name;
  }
 
  public void setName(String name) {
    this.name = name;
    notifyObservers();
  }
 
  public String getName() {
    return name;
  }
 
  public void addObserver(SubjectObserver observer) {
    observers.add(observer);
  }
 
  public void deleteObserver(SubjectObserver observer) {
    observers.remove(observer);
  }
 
  private void notifyObservers(){
    observers.stream().forEach(observer -> observer.update());
  }
}

User类中,当通过setter方法变更其状态事,都会触发调用绑定到它的观察者。

使用主题观察者和主题,以下是实例给出了观察方式:


public static void main(String[] args) {
  User user = new User("John");
  user.addObserver(() -> System.out.println(
      "Observable subject " + user + " has changed its state."));
  user.setName("Jack");
}

每当User对象的状态通过setter方法进行修改时,观察者将被通知并向控制台打印出一条消息。到目前为止,给出了观察者模式的一个简单用例。不过,通过这个看似简单的用例,我们了解到在这种情况下控制是如何实现反转的。

观察者模式下,主题就是起到”框架层“的作用,它完全主导何时何地去触发谁的调用。观察者的主动权被外放,因为观察者无法主导自己何时被调用(只要它们已经被注册到某个主题中的话)。这意味着,实际上我们可以发现控制被反转的”事发地“ – - – 当观察者绑定到主题时:


user.addObserver(() -> System.out.println(
      "Observable subject " + user + " has changed its state."));

上述用例,简要说明了为什么,观察者模式(或GUI驱动环境中的action监听器)是实现IoC的一种非常简单的方式。正是以这种分散式设计软件组件的形式,使得控制得以发生反转。

通过模板方法模式实现IoC

模板方法模式实现的思想是在一个基类中通过几个抽象方法(也称算法步骤)来定义一个通用的算法,然后让子类提供具体的实现,这样保证算法结构不变。

我们可以应用这个思想,定义一个通用的算法来处理领域实体:


public abstract class EntityProcessor {
 
  public final void processEntity() {
    getEntityData();
    createEntity();
    validateEntity();
    persistEntity();
  }
 
  protected abstract void getEntityData();
  protected abstract void createEntity();
  protected abstract void validateEntity();
  protected abstract void persistEntity();
 
}

processEntity() 方法是个模板方法,它定义了处理实体的算法,而抽象方法代表了算法的步骤,它们必须在子类中实现。通过多次继承 EntityProcessor 并实现不同的抽象方法,可以实现若干算法版本。

虽然这说清楚了模板方法模式背后的动机,但人们可能想知道为什么这是 IoC 的模式。

In typical inheritance, the subclass calls the method defined in the base class. In this mode, the relatively real situation is: the method (algorithm step) implemented by the subclass is called by the template method of the base class. Therefore, the control is actually in the base class, not in the subclass.
This is also a typical example of IoC, implemented through a layered structure. In this case, the template method is just a fancy name for a tunable extension point that developers use to manage their own set of implementations.

Summary

Although inversion of control is ubiquitous in the Java ecosystem, especially many frameworks commonly use dependency injection, for most developers However, this pattern is still vague and its application is limited by dependency injection. In this article, I illustrate this concept by showing several practical ways to implement IoC.

Dependency Injection: Control of obtaining dependencies from the client no longer exists in these classes. It is handled by the underlying injector/DI framework.
Observer pattern: When the subject changes, control is passed from the observer to the subject.
Template Method Pattern: Control occurs in the base class that defines the template method, rather than in the subclass that implements the algorithm steps.
As always, how and when to use IoC is determined by analyzing each use case, don't IoC for IoC's sake.

The above is the detailed content of Detailed explanation of how Java uses three design patterns of IOC inversion of control. For more information, please follow other related articles on the PHP Chinese website!

Statement:
The content of this article is voluntarily contributed by netizens, and the copyright belongs to the original author. This site does not assume corresponding legal responsibility. If you find any content suspected of plagiarism or infringement, please contact admin@php.cn