Interface Segregation Principle.

We have heard about this design principle in software development. Let’s get straight into a problem and try and understand what this principle says!

Let us design an ATM (Automated Teller Machine). An ATM has transactions. A transaction can be WithdrawalTransaction, DepositTransaction, TransferTransaction etc. Next, we have a UI for ATM, and UI can be Braille UI, Screen UI and Speech UI. Different transactions can speak to UI methods to publish their message, however, the message might differ based on a transaction. Having a single UI interface having RequestTransferAmount, RequestDepositAmount that can work with all the types wouldn’t be the right solution here. It is because, a type of transaction, say DepositTransaction, can enforce a change to UI interface to add a new functionality, which would further lead to changes in all the other transactions that extended the same UI. In short our the problem is:

“A change in UI for solving a problem withDepositTransaction resulted in changes with TransferTransaction.

It looks like we need a UI for Withdraw, Deposit and Transfer.i.e, DepositUI, WithdrawUI and TransferUI.

First stage of Solution:

Intent of doing this, clients should not be forced to depend on interfaces they don’t use. This is called interface segragation principle

So, here we go: A part of the solution:

interface DepositUI {
    public int requestDepositAmount();
}

interface WithDrawUI {
    public int requestWithDrawAmount();
}

interface UI extends DepositUI, WithDrawUI {
    
}

interface Transaction {
    public void execute();
}

class DepositTransaction implements Transaction {
    private DepositUI ui;

   public DepositTransaction(DepositUI ui) {
        this.ui = ui;
   }
    
   public void someCommonTransactionMethod(){
        // ..some implementation
   }

   public void execute() {
        ui.requestDepositAmount();
    }
}

class WithDrawTransaction implements  Transaction {
   private WithDrawUi ui;

   public WithDrawTransaction(WithDrawUi ui){
        this.ui = ui;
   }

   public void execute() {
     ui.requestWithDrawAmount();
   }
}

This is it!

A few more points to note

You may note that, DepositTransaction must know about DepositUI and Withdraw Transaction must know about WithDrawUI. As you can see, we solved this problem by making the constructor of each type of transaction expecting the right type of UI. We could pass in the UI as given below

class ATM {

   UI gui;  //global object;

   DepositTransaction dt = new DepositTransaction(gui);
}

Another way of handling this problem is to have a global package listing down all types of UIs. i.e, static global objects. Hence we can avoid passing in the UI during the construction of different transactions. However, they are two different approaches that allow us to follow Interface Segregation Principle. Please note that if these globals are put into a class instead of a package, that would be violation of the principle as we are in a way combining all the interfaces together when we import this class to work with a specified transaction. Personally, I would go with the solution given in the above example, that is, pass the right UI to the right transaction.

The polyad and monad - Monadic is not always the best approach

Assume that, We might be in need of a function f that has to access both DepositUI and WithDrawUI. We could separately pass these UIs to f as given below.

  
  f(WithdrawUI, DepositUI)

We made the function polyadic, and passed multiple parameters into it. Let me quote a sentence from the book Clean Code written by Robert.C.Martin here

A function with two arguments is harder to understand than a monadic function. For example, writeField(name) is easier to understand than writeField(output-stream, name). Dyads aren’t evil, and you will certainly have to write them. However, you should be aware that they come at a cost and should take advantage of what mechanism may be available to you to convert them into monads.

In the above example, you might have noted that we are mixing in various UIs to have a UI type. Following the above quotes let us make our function monadic. (You may note that UI in our example is a mix of all interfaces)

  
  f(UI)

Here we made it monadic, however it comes with cost - it drifted us away from following our interface segregation principle. That is, any change in any of the interface affects the function f and all of its clients forcing it to recompile. Hence we prefer polyadic approach here..