Lets say we have class A in package A and class B in package B . If object of class A has reference to class B, then the two classes are said to have coupling between them.
To address the coupling, it is recommended to define an interface in package A which is implemented by class in package B. Then object of class A can refer to interface in package A . This is often an example in "inversion of dependency".
Is this the example of "decoupling two classes at the interface level". If yes, how does it remove the coupling between classes and retain the same functionality when two classes were coupled?
Decoupled, or decoupling, is a state of an IT environment in which two or more systems somehow work or are connected without being directly connected. In a decoupled microservices architecture, for example, software services have none or very little knowledge about the other services.
decoupling allows the separation of object interaction from classes and inheritance into distinct layers of abstraction used to polymorphic-ally decouple the encapsulation which is the practice of using re-usable code to prevent discrete code modules from interacting with each other.
In formal design, decoupling means to make the features of a formal system as independent as possible from each other. Decoupling tends to make the features semantically more primitive and the overall system more general.
Decoupling is a coding strategy that involves taking the key parts of your classes' functionality (specifically the hard-to-test parts) and replacing them with calls to an interface reference of your own design. With decoupling, you can instantiate two classes that implement that interface.
Let us create a fictive example of two classes A
and B
.
Class A
in package packageA
:
package packageA;
import packageB.B;
public class A {
private B myB;
public A() {
this.myB = new B();
}
public void doSomethingThatUsesB() {
System.out.println("Doing things with myB");
this.myB.doSomething();
}
}
Class B
in package packageB
:
package packageB;
public class B {
public void doSomething() {
System.out.println("B did something.");
}
}
As we see, A
depends on B
. Without B
, A
cannot be used. We say that A
is tightly coupled to B
. What if we want to replace B
in the future by a BetterB
? For this, we create an Interface Inter
within packageA
:
package packageA;
public interface Inter {
public void doSomething();
}
To utilize this interface, we
import packageA.Inter;
and let B implements Inter
in B
andB
within A
with Inter
.The result is this modified version of A
:
package packageA;
public class A {
private Inter myInter;
public A() {
this.myInter = ???; // What to do here?
}
public void doSomethingThatUsesInter() {
System.out.println("Doing things with myInter");
this.myInter.doSomething();
}
}
We can see already that the dependency from A
to B
is gone: the import packageB.B;
is no longer needed. There is just one problem: we cannot instantiate an instance of an interface. But Inversion of control comes to the rescue: instead of instantiating something of type Inter
within A
's constructor, the constructor will demand something that implements Inter
as parameter:
package packageA;
public class A {
private Inter myInter;
public A(Inter myInter) {
this.myInter = myInter;
}
public void doSomethingThatUsesInter() {
System.out.println("Doing things with myInter");
this.myInter.doSomething();
}
}
With this approach we can now change the concrete implementation of Inter
within A
at will. Suppose we write a new class BetterB
:
package packageB;
import packageA.Inter;
public class BetterB implements Inter {
@Override
public void doSomething() {
System.out.println("BetterB did something.");
}
}
Now we can instantiante A
s with different Inter
-implementations:
Inter b = new B();
A aWithB = new A(b);
aWithB.doSomethingThatUsesInter();
Inter betterB = new BetterB();
A aWithBetterB = new A(betterB);
aWithBetterB.doSomethingThatUsesInter();
And we did not have to change anything within A
. The code is now decoupled and we can change the concrete implementation of Inter
at will, as long as the contract(s) of Inter
is (are) satisfied. Most notably, we can support code that will be written in the future and implements Inter
.
Adendum
I wrote this answer in 2015. While being overall satisfied with the answer, I always thought that something was missing and I think I finally know what it was. The following is not necessary to understand the answer, but is meant to spark interest in the reader, as well as provide some resources for further self-education.
In literature, this approach is known as Interface segregation principle and belongs to the SOLID-principles. There is a nice talk from uncle Bob on YouTube (the interesting bit is about 15 minutes long) showing how polymorphism and interfaces can be used to let the compile-time dependency point against the flow of control (viewer's discretion is advised, uncle Bob will mildly rant about Java). This, in return, means that the high level implementation does not need to know about lower level implementations when they are segretaget through interfaces. Thus lower levels can be swapped at will, as we have shown above.
Imagine that the functionality of B
is to write a log to some database. The class B
depends on the functionality of the class DB
and provides some interface for its logging functionality to other classes.
Class A
needs the logging functionality of B
, but is does not care, where the log is written to. It does not care for DB
, but since it depends on B
, it also depends on DB
. This is not very desirable.
So what you can do, is to split the class B
into two classes: An abstract class L
describing the logging functionality (and not depending on DB
), and the implementation depending on DB
.
Then you can decouple the class A
from B
, because now A
will only depend on L
. B
now also depends on L
, that is why it is called dependency inversion, because B
provides the functionality offered in L
.
Since A
now depends on just a lean L
, you can easily use it with other logging mechanism, not depending on DB
. E.g. you can create a simple console based logger, implementing the interface defined in L
.
But since now A
does not depend on B
but (in sources) only on the abstract interface L
at run time it has to be set up to use some specific implementation of L
(B
for instance). So there needs to be somebody else that tells A
to use B
(or something else) during the runtime. And that is called inversion of control, because before A
decided to use B
, but now somebody else (e.g. a container) tells A
to use B
during the runtime.
The situation you describe removes the dependence that class A has on the specific implementation of class B and replaces it with an interface. Now class A can accept any object that is of a type that implements the interface, instead of only accepting class B. The design retains the same functionality because class B is made to implement that interface.
This is where DI (Dependency Injection) frameworks really shine.
When you are building interfaces, you are actually building out contracts for implementation. Your calling services will only interact with the contract and the promise that a service interface will always provide the methods that it has specified.
For example...
Your ServiceA
will build their logic around ServiceB
's interface and does not have to worry about what happens under ServiceB
's hood.
This allows you to create multiple implementations of ServiceB
without having to change any logic in ServiceA
.
For the sake of example
interface ServiceB { void doMethod() }
You can interact with ServiceB in ServiceA without knowing what goes under the hood of ServiceB.
class ServiceAImpl {
private final ServiceB serviceB;
public ServiceAImpl(ServiceBImpl serviceBImpl) {
this.serviceB = serviceBImpl
}
public void doSomething() {
serviceB.doMethod(); // calls ServiceB interface method.
}
}
Now because you have built ServiceA
using the contract specified in ServiceB
, you are able to change out the implementation as you please.
You can mock the service, create different connection logic to different databases, create different runtime logic. All of these can change and will not at all affect the way ServiceA
interacts with ServiceB
.
Thus, loose coupling is achieved with IoC (Inversion of Control). You now have a modular and focused codebase.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With