Developers who have used Spring should have a good understanding of IOC inversion control. At the beginning of learning, they should know how to use dependency injection to implement IOC function. This paper introduces several design patterns of IOC inversion control.
Dependency Injection to Implement IOC
Injection dependency is one of the most basic ways of IOC implementation and one of the most commonly used object-oriented design methods. How injection dependency achieves control inversion effect begins with an example:
public interface UserQueue { void add(User user); void remove(User user); User get(); } public abstract class AbstractUserQueue implements UserQueue { protected LinkedList<User> 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 common method for storing User objects in a queue. AbstractUserQueue provides some common method implementations for subsequent inheritance classes. The final User FifoQueue and User LifoQueue implements FIFO and LIFO queues, respectively.
This is an effective way to realize subclass polymorphism.
By creating a client class that relies on the UserQueue abstract type (also known as service in DI terminology), different implementations can be injected at runtime without refactoring the 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 that dependency injection is indeed a way of IOC.
We can instantiate the dependencies on queues in the UserProcessor directly in the constructor through some hard-coded methods such as new operations. But this is typical code hard programming, which introduces strong coupling between client classes and their dependencies, and greatly reduces testability.
This class declares its dependency on the abstract class UserQueue in the constructor. That is to say, dependencies are no longer based on the use of new operations in constructors. Instead, dependencies are injected externally, either using a dependency injection framework or factory or builders mode.
With dependency injection, the dependency control of client classes is no longer in these classes, but in the injector, see the following code:
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(); }
The above approach achieves the desired results, and the injection of UserLifoQueue is simple and clear.
Implementing IOC in Observer Mode
It is also a common intuitive way to implement IOC directly through observer mode. Generally speaking, the observer pattern is usually used to track the change of state of model objects in the context of model view through the implementation of IOC by observers.
In a typical implementation, one or more observers are bound to observable objects (also known as topics in pattern terminology), such as by calling the addObserver method. Once the binding between the observer and the observer is defined, the change of the observer's state triggers the operation calling the observer. Look at the following examples:
public interface SubjectObserver { void update(); }
When the value changes, it triggers the call to this very simple observer. In reality, API s with richer functions, such as saving changed instances or old and new values, are often provided, but these do not require observing action (behavior) patterns, so here's an example as simple as possible.
Next, an observer class is given:
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()); } }
In the User class, when the setter method changes its state event, it triggers the call to the observer bound to it.
Using subject observers and topics, the following examples show how to observe:
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 object is modified by the setter method, the observer is notified and a message is printed to the console. So far, a simple use case of the observer pattern has been given. However, through this seemingly simple use case, we understand how control reverses in this case.
In the observer mode, the theme acts as a "framework layer", which completely dominates when, where and who calls. The observer's initiative is externalized because the observer cannot decide when he or she will be invoked (as long as they have been registered with a topic). This means that, in fact, we can find "the place where the control is reversed" - when the observer is bound to the subject:
user.addObserver(() -> System.out.println(
"Observable subject " + user + " has changed its state."));
The above use cases briefly illustrate why the observer pattern is a very simple way to implement IoC. It is in this form of decentralized design of software components that control can be reversed.
IMPLEMENTATION OF IOC WITH TEMPLATE METHOD PATTERN
The idea of template method pattern implementation is to define a general algorithm in a class by several abstract methods, and then let subclasses provide specific implementation, so as to ensure that the algorithm structure remains unchanged.
We can apply this idea to define a general algorithm to deal with domain entities. See examples:
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 a template method that defines algorithms for processing entities, while abstract methods represent steps of algorithms that must be implemented in subclasses. Several algorithmic versions can be implemented by inheriting EntityProcessor several times and implementing different abstraction methods.
Although this clarifies the motivation behind the template approach pattern, one may wonder why this is the IOC pattern.
In typical inheritance, subclasses call methods defined in base classes. In this mode, the relatively real situation is that the method of subclass implementation (algorithm steps) is called by template method of base class. Therefore, control is actually done in the base class, not in the subclass.
Summary:
Dependency injection: Control of obtaining dependencies from clients no longer exists in these classes. It is processed by the underlying injector / DI framework.
Observer model: When the subject changes, control is transferred from the observer to the subject.
Template Method Patterns: Control occurs in the base class that defines the template method, not in the subclass that implements the algorithm steps.