Sarit Adhikari Sarit Adhikari - 1 month ago 13
Java Question

What does decoupling two classes at the interface level mean?

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?

Answer

Let us create a fictive example.

Class A in package packageA:

package packageA;

import packageB.B;

public class A {
    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 you see, A depends on B. Without B, A cannot be used. But what, if we want to replace B in the future by a BetterB? Now, we start to 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 and change all occurences of B within A by Inter. The result is this modified version of A:

package packageA;

public class A {
    Inter myInter;

    public A() {
        this.myInter = ???; // What to do here?
    }

    public void doSomethingThatUsesInter() {
        System.out.println("Doing things with myInter");
        this.myInter.doSomething();
    }
}

At this point, we 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 wihtin A's constructor, we will demand something that implements Inter as parameter for the constructor:

package packageA;

public class A {
    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 you write a new class BetterB like this:

package packageB;

import packageA.Inter;

public class BetterB implements Inter {
    @Override
    public void doSomething() {
        System.out.println("BetterB did something.");
    }
}

Now, we can instantiante As 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 you can change the concrete implementation of Inter at will, as long as the contract(s) of Inter is (are) satisfied. Most notably, you can support code, which will be generated in the future and implements Inter.