Friday, September 20, 2024

SOLID Principles: Interface Segregation Principle



In this series, I will highlight some areas where you can improve the quality of your code during the development process. In the first article, I explained why code quality is paramount. Then I introduced briefly some ideas that would help you improve the quality of your code. I will provide more details on these points in future articles. Some concepts will be illustrated with examples in Java and Python. I will begin this journey with you by introducing the SOLID principles in this article. They are some of the concepts that will help you improve the quality of your code and boost your abstract thinking in the object-oriented paradigm. After introducing the Single Responsibility Principle, Open/Closed Principle, and Liskov Substitution Principle in previous articles, I will elucidate the Interface Segregation Principle in this article with examples in Java and Python.


1. Interface Segregation Principle (ISP)

This principle stands for having a preference for several client-specific interfaces rather than one general-purpose interface. In other words, interfaces should be broken down into more specific interfaces. Each one wraps properties and methods they have strong cohesion. Thus, a class can implement one or more interfaces depending on its needs. It means a class should not implement features it doesn’t use. 


By creating small interfaces, you should always favor decoupling over coupling and composition over inheritance. Adhering to the Interface Segregation Principle helps to achieve several benefits in terms of code aspects that we explain shortly here:


Loose Coupling and Strong Cohesion: This principle promotes breaking a large, general-purpose interface into smaller interfaces. Then, a class will implement only the interfaces that contain what is relevant to it. This makes the system more flexible. Moreover, a class won’t depend on features it doesn’t need. This reduces the coupling between classes.


Maintainability: As classes implement only the methods that are relevant to them, you get much better clarity in maintaining the code.


Modularity and Scalability: Adhering to ISP helps create more modular software systems because this principle favors decomposing large interfaces into small ones (modularity) and using each feature as needed (scalability). 


Extensibility: Adding new features becomes easy because the functionalities to be implemented are encapsulated in small interfaces. These are tailored to the specific needs of the client.


Reusability: As classes only contain highly cohesive elements, they become more reusable.


Readability: Classes do not implement useless code, so the code is more readable.


Testability: Writing test cases for classes that implement a few small interfaces is easy.


2. Example of ISP in Java

Java supports the concept of interface and incorporates the syntax to implement it. To illustrate how to adhere to the ISP in Java, let’s take an ‘Employee’ class that extends a ‘Person’ class as follows:


public class Person {

    String name;

    Date birthdate;

    public Person(String name, Date birthdate) {

        this.name = name;

        this.birthdate = birthdate;

    }

}


public class Employee extends Person {

    String email;

    String phone;

    double salary;

    public Employee(String name, Date birthdate, String email, 

String phone, double salary) {

        super(name, birthdate);

        this.email = email;

        this.phone = phone;

        this.salary = salary;

    }

    double calculateBonus(double salary, double coefficient){

        return (salary * coefficient) / 100;

    }

}


However, not all employees are eligible for a bonus. In most cases, temporary workers do not receive a bonus. Indeed, the ‘calculateBonus’ method is not tightly coupled to the other properties and methods of the “Employee” class.


To overcome this problem, you should adhere to the Interface Segregation Principle, which makes your code more flexible and reusable. For that, we separate the loosely coupled elements into different classes and interfaces. So, we single out the method ‘calculateBonus’ and encapsulate it in another interface, which we call ‘EmployeeBonus’:


public interface EmployeeBonus {

    double calculateBonus(double coefficient);

}


Now the ‘Employee’ class will have only the basic properties:


public class Employee extends Person {

    String email;

    String phone;

    Double salary;

    public Employee(String name, Date birthdate, String email, 

String phone, double salary) {

        super(name, birthdate);

        this.email = email;

        this.phone = phone;

        this.salary = salary;

    }

}


We now have a lot of flexibility to create classes that implement different functionality as needed. In our example, we can propose a class for employees who benefit from a bonus by implementing the ‘EmployeeBonus’ interface:


public class PermanentEmployee extends Employee implements EmployeeBonus {

    public PermanentEmployee(String name, Date birthdate, String email, 

String phone, double salary) {

        super(name, birthdate, email, phone, salary);

    }

    @Override

    public double calculateBonus(double coefficient) {

        return (salary * coefficient) / 100;

    }

}


In the main method of the ‘EmployeeDemo’ class, we create an instance of the ‘PermanentEmployee’ class and we call its ‘calculateBonus’ method:


import java.text.ParseException;

import java.text.SimpleDateFormat;

import java.util.Date;

import java.util.Locale;


public class EmployeeDemo {

    public static void main(String[] args) throws ParseException {

        SimpleDateFormat formatter = new SimpleDateFormat("dd-MMM-yyyy", 

Locale.ENGLISH);

        Date birthdate = formatter.parse("3-Jun-1995");

        PermanentEmployee employee = new PermanentEmployee("Emily Doe", birthdate,

                "emilydoe@gmail.com", "055566678", 50000.0);

        double bonus = employee.calculateBonus(0.25);

        System.out.println("Bonus for the employee Emily Doe: " + bonus);

    }

}


Running the example above gives us the following output:


Bonus for the employee Emily Doe: 125.0


3. Example of ISP in Python

Python does not have a special keyword to define an interface like Java but supports this concept. A Python interface is a class that contains methods that can be overridden. Consider a Python example of a printer interface with two methods ‘print_document’ and ‘scan_document’.


class Printer:

    def __init__(self):

        pass


    def print_document(self):

        pass


    def scan_document(self):

        pass


We define two classes ‘BasicPrinter’ and ‘MultifunctionPrinter’ to implement this interface as follows:


class BasicPrinter(Printer):

    def __init__(self):

        super().__init__()


    # Override  print_document

    def print_document(self):

        print("Printing document...")


class MultifunctionPrinter(Printer):

    def __init__(self):

        super().__init__()


    # Override  print_document

    def print_document(self):

        print("Printing document...")


    # Override  scan_document

    def scan_document(self):

        print("Scanning document…")


The ‘BasicPrinter’ and ‘MultifunctionPrinter’ classes implement all the methods of the ‘Printer’ interface. However, the ‘BasicPrinter’ class has the printing function but not the scanning and normally does not support the ‘scan_document’ method. So this example violates the Interface Segregation Principle.


To respect this principle, we should refactor the example and create more specific interfaces rather than a single large interface. We split the ‘Printer’ interface into two specific interfaces which we called ‘Printer’ and ‘Scanner’:


class Printer:

    def __init__(self):

        pass


    def print_document(self):

        pass



class Scanner:

    def __init__(self):

        pass


    def scan_document(self):

        pass


Now the ‘BasicPrinter’ class can implement only ‘Printer’ and the ‘MultifunctionPrinter’ class can implement the two interfaces as follows:


class BasicPrinter(Printer):

    # Override  print_document

    def print_document(self):

        print("Printing document...")


class MultifunctionPrinter(Printer, Scanner):

    # Override  print_document

    def print_document(self):

        print("Printing document...")


    # Override  scan_document

    def scan_document(self):

        print("Scanning document…")


# Create instances of BasicPrinter and MultifunctionPrinter and call their methods

basicPrinter = BasicPrinter()

basicPrinter.print_document()


multifunctionPrinter = MultifunctionPrinter()

multifunctionPrinter.print_document()

multifunctionPrinter.scan_document()


Running the code above gives us the following result:


Printing document...

Printing document...

Scanning document...


Our system now adheres to the Interface Segregation Principle because each class implements only the relevant methods.

No comments:

Post a Comment

Blog Posts

Enhancing Performance of Java-Web Applications

Applications built with a Java back-end, a relational database (such as Oracle or MySQL), and a JavaScript-based front-end form a common and...