26Jun
    Introducing Interface Segregation Principle

    Interface Segregation Principle (ISP) is the fourth principle in S.O.L.I.D. set of principles (hence the letter I) and it deals with how business logic is communicated to the client. In object-oriented world, business logic is implemented in classes and exposed to clients using abstractions like interfaces. The client use the interface to call the appropriate methods without care about the object that implements them (Liskov Substitution Principle). In this way the two parts can evolve separately, as long as the contract between them, represented by the interface, does not change.

    What happens if the interface is not cohesive? What if it contains methods that can be grouped, and each group is serving a different set of clients?. It means that a client, although it is using only some of the methods, has also a dependency on the others, since they are part of the same interface. These interfaces are called “fat” interfaces.

    There are objects that are requiring non-cohesive interfaces. Sometimes this cannot be avoided. However, it is important that clients should not know them as a single class. Instead, clients should only deal with abstractions (like interfaces or abstract classes) which have cohesive interfaces, even though that internally there is a single class providing all the implementations. This is what the principle is stating: clients should not be forced to depend upon interfaces that they do not use.

    Let’s consider an example: we want to manage big numbers (those outside the ranges of the standard numeric types) and we want to implement the following functions:

    • •    the basic arithmetic operations,
    • •    compare big numbers between them
    • •    to save and restore them from a persistent store.

    The interface of such a class would be:

    interface BigNumber {
    public BigNumber add(BigNumber number);
    public BigNumber multiply(BigNumber number);
    public BigNumber substract(BigNumber number);
    public BigNumber divide(BigNumber number);
    public int compareTo(BigNumber number);
    public void readBigNumber(InputStream stream);
    public void writeBigNumber(OutputStream stream);
    }

    One can see we have three groups of methods: one group contains methods that are dealing with arithmetic operations, another group contains the method used to compare two objects and a third one contains methods to save and restore big numbers from a persistent store.

    Clients that are using a BigNumber implementation might use all methods or are only concerned a part of them: for example, a module which does calculations will deal with the methods that implements arithmetic operations, while there might be a different module that manages a persistent store and therefore will only interact with the methods related to the save and restore of the number. Nevertheless, all these clients will refer to the same interface when working with big numbers.

    What kind of problems might arise? For example, there might be a need for an additional parameter in the method divide, to specify the number of decimals to round up.

    interface BigNumber {
    // …
    public BigNumber divide(BigNumber number, int noOfDecimals);
    // …
    }

    The method signature will be changed, and this affects all the implementations of the interface but also all the clients, no matter if they are actually working or not with that code, since all of them are referring the same interface, so the code needs to be at least re-compiled, even if no changes were made

    What would be solution? The solution is to segregate the interface into several smaller ones, each containing the group of methods used by one category of clients. Even though the underlining implementation is in the same class, the clients will only work with that interface that is relevant to them and they will not be affected by changes to the other interfaces.

    interface ArithmeticOperations {
    public BigNumber add(BigNumber number);
    public BigNumber multiply(BigNumber number);
    public BigNumber substract(BigNumber number);
    public BigNumber divide(BigNumber number);
    }

    interface BigNumberComparison {
    public int compareTo(BigNumber number);
    }

    interface PersistentBigNumber {
    public void readBigNumber(InputStream stream);
    public void writeBigNumber(OutputStream stream);
    }

    JAVA API contains two such classes that deals with big numbers: java.math.BigDecimal and java.math.BigInteger. Their implementation is a bit different from this example, but still the principle does apply:

    • there is no ArithmeticOperations interface, the actual classes are directly exposing these methods; which is fine because it is the core functionality of the class;
    • interface java.lang.Comparable is implemented, hence the compareTo method;
    • interface java.io.Serializable is implemented to serialize objects but here it is a bit different: the interface has no methods, is a markup interface. However, the Java SDK looks for implementations of particular methods, readObject and writeObject and if none found it provides an implementation by default. This is more loose application of the principle, in the sense that the specific methods for read and write are not included in an interface

    In this article we have discussed the disadvantages of “fat interfaces”, those interfaces that are not specific to a single client. Fat interfaces lead to inadvertent couplings beween clients that ought otherwise to be isolated and is best to segregate them into smaller interfaces or abstract classes in order eliminate this problem.