Three design patterns that use inversion of control

For many developers, inversion of control (IoC) is a fuzzy concept, with little to no real-world application. In the best case, it is considered to be a simple equivalent of addiction injection (DI). The IoC = DI The equation is only true, however, when both sides refer to the reversal of control of dependency management. While dependency injection is actually a well-known form of IoC, the truth is that IoC is a much larger software design paradigm, which can be implemented through several patterns. In this article, we’ll take a look at how dependency injection, the observer model, and the model method model implement inversion of control.

Like many other design patterns in the rich repertoire available, implementing IoC is a compromise for the developer:

  • Designing strongly decoupled components and encapsulating the business logic in one place are direct and natural consequences of implementing IoC.
  • The flip side is that the implementation requires the construction of at least one layer of diversion and in some use cases it can be overkill.

Examining a few concrete implementations will help you to compromise between these properties.

Demystifying the IoC Paradigm

The inversion of control is a model with several tilts. A typical example of IoC is donated by Martin Fowler in the following simple program that collects 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 that case, the program control flow is defined by the main method: In an infinite loop, it reads user input and prints it to the console. Here, the method has full control over when to read user input and when to print it.

Now, consider a revamped version of the program, which uses a graphical user interface (GUI) to collect input data through a text field box, button, and action listener linked to it. In this context, each time the user clicks on the button, the entered text is collected by the listener and printed on a panel.

In this version of the program, it’s actually under the control of the event listener model (in this case it’s the framework) to call code written by the developer to read and print user input. Simply put, the framework will call the developer’s code, rather than the other way around. The framework is actually an extensible framework that provides the developer with a specific set of points to inject custom code segments.

In this sense, control has effectively been reversed.

From a more generic point of view, each callable extension point defined by a framework, either in the form of interface implementation (s), implementation inheritance (aka subclassification) is a well-defined form of IoC.

Take the case of a simple Servlet:

public class MyServlet extends HttpServlet {

    protected void doPost(
            HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        
    }

    protected void doGet(
            HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        
    }

}

Here the HttpServlet class (belonging to the framework) is the element that has full control of the program, not the MyServlet subclass. The code in the doGet() and doPost() the methods are called automatically in response to TO HAVE and PUBLISH HTTP requests by the servlet after being created by the servlet container.

Compared to a typical inheritance perspective, where subclasses have control instead of the base class, control has been reversed.

In fact, the methods of the servlet are an implementation of the template method model, which we will discuss in detail later.

In the case of frames that stick to open / closed principle By providing an extensible API, the role of the developer using the framework boils down to defining their own set of custom classes, either by implementing one or more interfaces provided by the framework, or by inheriting existing base classes. In turn, the instances of the classes are directly instantiated and called by the framework.

To quote Fowler:

The framework calls the developer, rather than the developer calling the framework.

This is why IoC is often referred to as the Hollywood Principle:

Don’t call us, we’ll call you.

Implementing inversion of control

At this point, it becomes evident that there are several methodologies for implementing the reverse control. Let’s go over how to implement the most common ones.

IoC via dependency injection

As stated earlier, DI is just one form of IoC, and probably one of the most ubiquitous used in object-oriented design. But let’s think about this: How does DI actually reverse control?

To answer that, let’s create a naive 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 defines the public API of a simple queue that stores user objects (the implementation of the User class is omitted here for brevity), while AbstractUserQueue provides a shared implementation lower in the hierarchy. Recently, UserFifoQueue and UserLifoQueue, implement the base FIFO and LIFO Waiting lines.

This is an efficient way to implement subtype polymorphism. But what does this buy us concretely? Pretty much, actually.

By creating a client class that declares a dependency on a UserQueue abstract type (aka a service in DI terminology), different implementations can be injected at run time, without refactoring code that uses the client class:

public class UserProcessor {

    private UserQueue userQueue;

    public UserProcessor(UserQueue userQueue) {
        this.userQueue = userQueue;
    }

    public void process() {
        // process queued users here
    }

}

UserProcessor shows in a nutshell why DI is actually a form of IoC.

We could have placed control over how to acquire the dependency on the queue in UserProcessor by instantiating them directly in the constructor, via new the operators. But it’s the typical smell of code that introduces a strong coupling between client classes and their dependencies and kills testability. I hear alarm bells ringing in my ears! Is not it ? Yes, that would be a bad design.

Instead, the class declares a dependency on the abstract type UserQueue in the constructor, so it is no longer under his control to search for his collaborator through a new operator in constructor, Conversely, the dependency is injected from the outside, either by using a DI framework (CDI and Google Guice are fine examples of external injectors), or just old-fashioned factories and builders.

In a word, with DI, control over how dependencies are acquired by client classes no longer resides in those classes; it resides in the injectors instead:

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

It will work as expected and by injecting the UserLifoQueue the implementation is also quite simple. Clearly, DI is just a way to achieve reverse control (in this case DI is a layer of indirection for the IoC implementation).

IoC via the observer model

Another easy way to implement IoC is to observer model. In a broader sense, the way observers reverse control is similar to that of an action listener in the context of a GUI. While in the case of action listeners, they are called in response to a specific user event (a mouse click, multiple keyboard / window events, etc.), observers are typically used to track changes in state. ‘a model object. in a model view context.

In a classic implementation, one or more observers are linked to the observable object (aka the object in model terminology), for example by calling a addObserver method. Once the links between the subject and the observer (s) have been defined, the observers are called in response to a change in the subject’s state.

To better understand this concept, consider the following example:

@FunctionalInterface
public interface SubjectObserver {

    void update();

}

This very simple observer is called whenever a value changes. In real life it should provide a richer API, for example containing the modified instance or the old and the new value, but these are not needed to watch the model in action, so I did simple.

Here is an observable class:

public class User {

    private String name;
    private List<SubjectObserver> 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());
    }

}

The User class is just a naive domain class, which notifies the attached observer (s) whenever its state is changed via setter methods.

With the SubjectObserver interface and subject in place, here’s how an instance can be observed:

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

Whenever the state of the user the object is modified via the setter, the watcher is notified and therefore it prints a message to the console. So far this has been a fairly trivial application of the observer model. What is not so trivial, however, is how the control is reversed in this case.

With the observer model, the subject becomes “the executive” who exercises control over who is called and when. These are the observers whose control is withdrawn because they have no influence on when they are called (as long as they remain registered with the subject). This means that we can actually spot the line where the control is reversed – this is when the observer is linked to the subject:

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

This shows in a nutshell why the observer model (or an action listener in a GUI driven environment) is a fairly straightforward way to achieve IoC. It is in this form of decentralized design of software components that the inversion of control takes place..

IoC via model method model

The motivation behind the model method model is to define a generic algorithm in a base class using several abstract methods (i.e. algorithm steps) and to let subclasses of it provide specific implementations, while keeping the structure of the algorithm unchanged.

We could apply this concept and define a generic algorithm for processing domain entities:

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

}

The processEntity() method is the model method which defines the structure of the algorithm which processes the entities, and the abstract methods are the steps of the algorithm, which must be implemented by the subclasses. Several versions of the algorithm could be created by sub-classification EntityProcessor as many times as needed, and providing different implementations of the abstract methods.

While this shows the motivations behind the model method model, it is questionable why the model is a form of IoC.

In a typical implementation of inheritance, subclasses call the methods defined in the base class. In this case, the reverse is actually true: the methods implemented by the subclasses (the steps of the algorithm) are called in the base class via the template method. So, it is in the base class where the control actually resides, not in the subclasses.

This is another classic example of IoC, achieved through a hierarchical structure. In that case, model method is just a fancy name to define a callable extension point, which is used by the developer to provide their own set of implementations.

Summary

Even though reverse control is prevalent in the Java ecosystem, especially in the many frameworks available and the ubiquitous adoption of dependency injection, for many developers the model is still vague and largely limited to the injection of dependencies. In this article, I have clarified this concept by presenting several approaches that can be taken to implement IoC in the real world.

  • Dependency injection: Control over how dependencies are acquired by client classes no longer resides in those classes. It instead resides in the underlying DI injectors / frames.

  • Observer model: Control is transferred from the observers to the subject when it comes to reacting to changes.

  • Model method model: Control resides in the base class that defines the model method, rather than in subclasses, implementing the steps of the algorithm.

As usual, how and when to use IoC is something that needs to be evaluated on a case-by-case basis, without falling into unnecessary blind worship.


Source link

Billie M. Secrist

Leave a Reply

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