Inversion of (Coupling) Control in Java
What is the inversion of control? And what is dependency injection? These types of questions are often met with code examples, vague explanations, and what has been identified on StackOverflow as 'low-quality answers.'
We use inversion of control and dependency injection and often push it as the correct way to build applications. Yet, we can not clearly articulate why!
The reason is we have not clearly identified what control is. Once we understand what we are inverting, the concept of inversion of control versus dependency injection is not actually the question to be asked. It actually becomes the following:
Inversion of Control = Dependency (state) Injection + Thread Injection + Continuation (function) Injection
To explain this, well, let's do some code. And yes, the apparent problem of using code to explain inversion of control is repeating, but bear with me, the answer has always been right before your eyes.
One clear use of inversion of control/ dependency injection is the repository pattern to avoid passing around a connection. Instead of the following:
public class NoDependencyInjectionRepository implements Repository<Entity> {
public void save(Entity entity, Connection connection) throws SQLException {
// Use connection to save entity to database
}
}
Dependency injection allows the repository to be re-implemented as:
public class DependencyInjectionRepository implements Repository<Entity> {
@Inject Connection connection;
public void save(Entity entity) throws SQLException {
// Use injected connection to save entity to database
}
}
Now, do you see the problem we just solved?
If you are thinking "I can now change the connection
to say REST calls," and this is all flexible to change, well, you would be close.
To see if the problem has been resolved, do not look at the implementation. Instead, look at the interface. The client calling code has gone from:
repository.save(entity, connection);
to the following:
repository.save(entity);
We have removed the coupling of the client code to provide a connection
on calling the method. By removing the coupling, we can substitute a different implementation of the repository (again, boring old news, but bear with me):
public class WebServiceRepository implements Repository<Entity> {
@Inject WebClient client;
public void save(Entity entity) {
// Use injected web client to save entity
}
}
With the client able to continue to call the method just the same:
repository.save(entity);
The client is unaware that the repository is now calling a microservice to save the entity rather than talking directly to a database. (Actually, the client is aware but we will come to that shortly.)
So, taking this to an abstract level regarding the method:
R method(P1 p1, P2 p2) throws E1, E2
// with dependency injection becomes
@Inject P1 p1;
@Inject P2 p2;
R method() throws E1, E2
The coupling of the client to provide arguments to the method is removed by dependency injection.
Now, do you see the four other problems of coupling?
At this point, I warn you that you will never look at the code the same again once I show you the coupling problems. This is the point in the Matrix where I ask you if you want to take the red or blue pill. There is no going back once I show you how far down the rabbit hole this problem really is — refactoring is actually not necessary and there are issues in the fundamentals of modeling logic and computer science (ok, big statement but read on — I can't put it any other way).
So, you chose the red pill.
Let's prepare you.
To identify the four extra coupling problems, let's look at the abstract method again:
@Inject P1 p1;
@Inject P2 p2;
R method() throws E1, E2
// and invoking it
try {
R result = object.method();
} catch (E1 | E2 ex) {
// handle exception
}
What is coupled by the client code?
The return type
The method name
The handling of exceptions
The thread provided to the method
Dependency injection allowed me to change the objects required by the method without changing the client code calling the method. However, if I want to change my implementing method by:
Changing its return type
Changing its name
Throwing a new exception (in the above case of swapping to a micro-service repository, throwing an HTTP exception rather than a SQL exception)
Using a different thread (pool) to execute the method than the thread provided by the client call
This involves "refactoring" all client code for my method. Why should the caller dictate the coupling when the implementation has the hard job of actually doing the functionality? We should actually invert the coupling so that the implementation can dictate the method signature (not the caller).
This is likely the point you look at me like Neo does in the Matrix going "huh"? Let implementations define their method signatures? But isn't the whole OO principle about overriding and implementing abstract method signature definitions? And that's just chaos because how do I call the method if it's return type, name, exceptions, arguments keep changing as the implementation evolves?
Easy. You already know the patterns. You just have not seen them used together where their sum becomes a lot more powerful than their parts.
So, let's walk through the five coupling points (return type, method name, arguments, exceptions, invoking thread) of the method and decouple them.
We have already seen dependency injection remove the argument coupling by the client, so one down.
Next, let's tackle the method name.
Method Name Decoupling
Many languages, including Java lambdas, allow or have functions as first class citizens of the language. By creating a function reference to a method, we no longer need to know the method name to invoke the method:
Runnable f1 = () -> object.method();
// Client call now decoupled from method name
f1.run()
We can even now pass different implementations of the method around dependency injection:
@Inject Runnable f1;
void clientCode() {
f1.run(); // to invoke the injected method
}
OK, this was a bit of extra code with not much additional value. But again, bear with me. We have decoupled the method's name from the caller.
Next, let's tackle the exceptions from the method.
Method Exceptions Decoupling
By using the above technique of injecting functions, we inject functions to handle exceptions:
Runnable f1 = () -> {
@Inject Consumer<E1> h1;
@Inject Consumer<E2> h2;
try {
object.method();
} catch (E1 e1) {
h1.accept(e1);
} catch (E2 e2) {
h2.accept(e2);
}
}
// Note: above is abstract pseudo code to identify the concept (and we will get to compiling code shortly)
Now, exceptions are no longer the client caller's problem. Injected methods now handle the exceptions decoupling the caller from having to handle exceptions.
Next, let's tackle the invoking thread.
Method's Invoking Thread Decoupling
By using an asynchronous function signature and injecting an Executor
, we can decouple the thread invoking the implenting method from that provided by the caller:
Runnable f1 = () -> {
@Inject Executor executor;
executor.execute(() -> {
object.method();
});
}
By injecting the appropriate Executor
, we can have the implementing method invoked by any thread pool we require. To re-use the client's invoking thread, we just use a synchronous Exectutor
:
Executor synchronous = (runnable) -> runnable.run();
So now, we can decouple a thread to execute the implementing method from the calling code's thread.
But with no return value, how do we pass state (objects) between methods? Let's combine it all together with dependency injection.
Inversion of Control (Coupling)
Let's combine the above patterns together with dependency injection to get the ManagedFunction
:
public interface ManagedFunction {
void run();
}
public class ManagedFunctionImpl implements ManagedFunction {
@Inject P1 p1;
@Inject P2 p2;
@Inject ManagedFunction f1; // other method implementations to invoke
@Inject ManagedFunction f2;
@Inject Consumer<E1> h1;
@Inject Consumer<E2> h2;
@Inject Executor executor;
@Override
public void run() {
executor.execute(() -> {
try {
implementation(p1, p2, f1, f2);
} catch (E1 e1) {
h1.accept(e1);
} catch (E2 e2) {
h2.accept(e2);
});
}
private void implementation(
P1 p1, P2 p2,
ManagedFunction f1, ManagedFunction f2
) throws E1, E2 {
// use dependency inject objects p1, p2
// invoke other methods via f1, f2
// allow throwing exceptions E1, E2
}
}
OK, there's a lot going on here but it's just the patterns above combined together. The client code is now completely decoupled from the method implementation, as it just runs:
@Inject ManagedFunction function;
public void clientCode() {
function.run();
}
The implementing method is now free to change without impacting the client calling code:
- There is no return type from methods (slight restriction always being
void
, however necessary for asynchronous code) The implementing method name may change, as it is wrapped by the
ManagedFunction.run()
Parameters are no longer required by the
ManagedFunction
. These are dependency-injected, allowing the implementing method to select which parameters (objects) it requiresExceptions are handled by injected
Consumers
. The implementing method may now dictate what exceptions it throws, requiring only differentConsumers
injected. The client calling code is unaware that the implementing method may now be throwing anHTTPException
instead of anSQLException
. Furthermore,Consumers
can actually be implemented byManagedFunctions
injecting the exception.The injection of the
Executor
allows the implementing method to dictate its thread of execution by specifying theExecutor
to inject. This could result in re-using the client's calling thread or have the implementation run by a separate thread or thread pool
All five coupling points of the method by its caller are now decoupled.
We have actually "Inverted Control of the Coupling." In other words, the client caller no longer dictates what the implementing method can be named, use as parameters, throw as exceptions, which thread to use, etc. Control of coupling is inverted so that the implementing method can dictate what it couples to by specifying it's a required injection.
Furthermore, as there is no coupling by the caller, there is no need to refactor code. The implementation changes and then configures in it's coupling (injection) to the rest of the system. Client calling code no longer needs to be refactored.
So, in effect, dependency injection only solved 1/5 of the method coupling problem. For something that is so successful for only solving 20 percent of the problem, it does show how much of a problem coupling of the method really is.
Implementing the above patterns would create more code than it's worth in your systems. That's why open-source OfficeFloor is the "true" inversion of control framework and has been put together to lessen the burden of this code. This has been an experiment in the above concepts to see if real systems are easier to build and maintain with "true" inversion of control.
Summary
So, the next time you reach for the Refactor Button/Command, realize that this is brought on by the coupling of the method that has been staring us in the face everytime we write code.
And really, why do we have the method signature? It is because of the thread stack. We need to load memory onto a thread stack, and the method signature follows the behavior of the computer. However, in the real world, the modeling of behavior between objects offers no thread stack. Objects are loosely coupled with very small touch points — not the five coupling aspects imposed by the method.
Furthermore, in computing, we strive towards low coupling and high cohesion. One might possibly put forward a case that,in comparison to ManagedFunctions
, methods are:
High coupling: methods have five aspects of coupling to the client calling code
Low cohesion: as the handling of exceptions and return types from methods starts blurring the responsibility of the methods over time, continuous change and shortcuts can quickly degrade the cohesiveness of the implementation of the method to start handling logic beyond its responsibility
Since we strive for low coupling and high cohesion, our most fundamental building block (the method
and the function
) may actually go against our most core programming principles.