Showing posts with label ISP. Show all posts
Showing posts with label ISP. Show all posts

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.

SOLID Principles: Single Responsibility 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. I will elucidate the first principle (Single Responsibility Principle), with examples in Java and Python.


1. SOLID principles

The SOLID principles are a collection of object-oriented design concepts for building a solid object-oriented software architecture. These principles help developers create software that are more readable, flexible, extensible, and maintainable.


It was Robert C. Martin who first introduced these principles. But it was Michael Feathers, who came up with the acronym SOLID and reworked these principles. The acronym SOLID stands for:


    • S: Single Responsibility Principle

    • O: Open/closed Principle

    • L: Liskov Substitution Principle

    • I: Interface Segregation Principle

    • D: Dependency Inversion Principle


2. Advantages of Using SOLID Principles

Compliance with SOLID principles has a big impact on the quality of your software. We can mention some benefits of using these principles as follows:


    • Reduce the complexity of the code.

    • Increase readability, extensibility, and maintenance.

    • Increase flexibility and reusability.

    • Reduce tight coupling.

    • Reduce errors for better testability.


We’ll show how using SOLID principles helps you develop a code with better quality that confirms the achievement of the advantages described above. In this series, we will explain each principle individually and illustrate with an example how each improves certain aspects of the software. But, we focus only on the Single Responsibility Principle (SRP)  in this article.


3. Single Responsibility Principle (SRP)

This principle means that a class should only have one responsibility. In other words, each class should only contain one feature or behavior. This principle helps you ensure code quality by preserving some of the code quality features we mention via the following benefits:


Testability: It is easy to define test cases for a class with a single responsibility.

Loose coupling: A class implementing SRP has few dependencies because it only encapsulates a single functionality.

Readability: This principle ensures that classes adhering to it are small, well-organized, and easy to understand.

Separation of Concerns: This principle preserves the concept of separation of concerns.

Maintainability: The ability to update the software is high.

Extensibility: Adding new features is easy.


4. Example of SRP in Java

To explain how a class complies with SRP or not, we create an illustrative class ‘Calculator’ with a few methods that we named ‘add’, ‘div’, ‘isMultipleOf’, and ‘removeWSpace’.

We defined the first three methods for arithmetic operations and followed strong logic to put them together (strong cohesion). 

The fourth operation removes white spaces from a variable of type string. That means it has a weak cohesion with the ‘add’, ‘div’, and ‘isMultipleOf’. 


Consider one responsibility according to our requirements, which is achieving mathematical operations. Therefore, this example violates the SRP because it does not encapsulate this sole responsibility. This could result in the loss of the benefits of the SRP mentioned above.


public class Calculator {

    public Calculator() { }

    public static int add(int x, int y) {

        return x + y;

    }

    public static int div(int x, int y) {

        return x / y;

    }

    public static boolean isMultipleOf(int x, int y) {

        if (x % y == 0) {

            return true;

        } else {

            return false;

        }

    }

    public static String removeWSpace(String inputStr) {

        return inputStr.replaceAll("\\s+","");

    }

}


5. Example of SRP in Python

Consider a class in Python that violates the SRP according to our requirements. We define a class named ‘ListExtremum’ which encapsulates four methods ‘find_max’, ‘find_min’, ‘find_max_dict’, and ‘find_min_dict’. 

The ‘find_max’ and ‘find_min’ methods look for the max and the min values respectively in a list. The ‘find_max_dict’ and ‘find_min_dict’ methods return the max and the min values respectively in a dictionary.

This example violates the SRP because it fulfills more than one responsibility. The first responsibility consists of calculating the extrema (minimum and maximum) of a list. The second one is to calculate the extrema of a dictionary.


class ListExtremum:

    def __init__(self, num_list, dict):

        self.num_list = num_list

        self.dict = dict


    def find_max(self):

        try:

            maximum = self.num_list[0]

            for number in self.num_list:

                if number > maximum:

                    maximum = number

            return maximum

        except TypeError:

            print("Method find_max - Error empty list")

        except IndexError:

            print("Method find_max - Error empty list")


    def find_min(self):

        try:

            minimum = self.num_list[0]

            for number in self.num_list:

                if number < minimum:

                    minimum = number

            return minimum

        except TypeError:

            print("Method find_min - Error empty list")

        except IndexError:

            print("Method find_min - Error empty list")


    def find_max_dict(self):

        try:

            max_key = max(self.dict, key=self.dict.get)

            max_value = self.dics[max_key]

            return max_value

        except AttributeError:

            print("Method find_max_dict - Error empty dictionary")

        except ValueError:

            print("Method find_max_dict - Error empty dictionary")


    def find_min_dict(self):

        try:

            min_key = min(self.dics, key=self.dics.get)

            min_value = self.dics[min_key]

            return min_value

        except AttributeError:

            print("Method find_min_dict - Error empty dictionary")

        except ValueError:

            print("Method find_min_dict - Error empty dictionary")




We explain what each method does in this code.
Constructor ‘__init__’: This code defines a constructor function (__init__) for the ‘ListExtremum’ class. It takes two arguments (‘num_list’ and ‘dict’) and assigns them to instance variables (‘self.num_list’ and ‘self.dict’).
Method ‘find_max’: this method searches for the maximum number in ‘self.num_list’. It initializes the maximum variable with the first element of ‘self.num_list’. Then, it iterates over the list and updates the maximum variable when it finds a number greater than the current maximum. It handles ‘TypeError’ and ‘IndexError’ exceptions and prints an error message if the list is None or empty.
Method ‘find_min’: This method returns the minimum value in ‘self.num_list’. It initializes the minimum variable with the element of ‘self.num_list’ at the first index. After that, it iterates over the list and updates the minimum variable when it finds a number smaller than the current minimum. It handles potential errors for an empty list by catching ‘TypeError’ and ‘IndexError’ exceptions and printing an error message.
Method ‘find_max_dict’: This method finds the key-value pair with the highest value in a dictionary. It uses the max function with the key parameter set to ‘self.dict.get’ to find the key with the highest value. If the dictionary is empty, it catches the ‘AttributeError’ and ‘ValueError’ exceptions and prints an error message.
Method ‘find_min_dict’: This method aims to find the key-value pair with the minimum value in a dictionary. It uses the min function to find the minimum key based on the values. Then, it retrieves the corresponding value from the dictionary. Like ‘find_max_dict’, this method catches the ‘AttributeError’ and ‘ValueError’ exceptions and prints an error message, if the dictionary is empty.
We should split this code into parts to make this class adhere to the SRP. The first part will implement operations applied to a list. The second part contains the operations of dictionaries. In this case, we get two classes called ‘ListExtremum’ and ‘DictExtremum’. 
The ‘ListExtremum’ class contains the ‘find_max’ and ‘find_min’ methods to calculate the max and min value of a list. On the other hand, the ‘DictExtremum’ class contains the ‘find_max_dict’ and ‘find_min_dict’ methods to get the max and min values of a dictionary.
class ListExtremum:
    def __init__(self, num_list):
        self.num_list = num_list
    def find_max(self):
        try:
            maximum = self.num_list[0]
            for number in self.num_list:
                if number > maximum:
                    maximum = number
            return maximum
        except TypeError:
            print("Method find_max - Error empty list")
        except IndexError:
            print("Method find_max - Error empty list")
    def find_min(self):
        try:
            minimum = self.num_list[0]
            for number in self.num_list:
                if number < minimum:
                    minimum = number
            return minimum
        except TypeError:
            print("Method find_min - Error empty list")
        except IndexError:
            print("Method find_min - Error empty list")

class DictExtremum:
    def __init__(self,  dict):
        self.dict = dict
    def find_max_dict(self):
        try:
            max_key = max(self.dict, key=self.dict.get)
            max_value = self.dict[max_key]
            return max_value
        except AttributeError:
            print("Method find_max_dict - Error empty dictionary")
        except ValueError:
            print("Method find_max_dict - Error empty dictionary")
    def find_min_dict(self):
        try:
            max_key = min(self.dics, key=self.dics.get)
            max_value = self.dics[max_key]
            return max_value
        except AttributeError:
            print("Method find_min_dict - Error empty dictionary")
        except ValueError:
            print("Method find_min_dict - Error empty dictionary")

We can call this code using some examples as follows:
print("**********************************************")
print("Calculate max and min values in None list and None dictionary")
print("")
list_extremum_calc = ListExtremum(None)
print("Max value in the list: ", list_extremum_calc.find_max())
print("Min value in the list: ", list_extremum_calc.find_min())
dict_extremum_calc = DictExtremum(None)
print("Max value in the dictionary: ", dict_extremum_calc.find_max_dict())
print("Min value in the dictionary: ", dict_extremum_calc.find_min_dict())
print("")
print("**********************************************")
print("Calculate max and min values in empty list and empty dictionary")
list_extremum_calc = ListExtremum([])
print("Max value in the list: ", list_extremum_calc.find_max())
print("Min value in the list: ", list_extremum_calc.find_min())
dict_extremum_calc = DictExtremum({})
print("Max value in the dictionary: ", dict_extremum_calc.find_max_dict())
print("Min value in the dictionary: ", dict_extremum_calc.find_min_dict())
print("")
print("**********************************************")
num_list = [18, 20, 25, -16, 27]
temp_dict = {'Vienna': 10, 'Paris': 11, 'Rome': 15, 'Algiers': 16}
print("num_list: ", num_list)
print("temp_dict: ", temp_dict)
print("Calculate max and min values in num_list and temp_dict")
print("")
list_extremum_calc = ListExtremum(num_list)
print("Max value in the list: ", list_extremum_calc.find_max())
print("Min value in the list: ", list_extremum_calc.find_min())
dict_extremum_calc = DictExtremum(temp_dict)
print("Max value in the dictionary: ", dict_extremum_calc.find_max_dict())
print("Min value in the dictionary: ", dict_extremum_calc.find_min_dict())

The execution of this code gives us the following result:
**********************************************
Calculate max and min values in None list and None dictionary
Method find_max - Error empty list
Max value in the list:  None
Method find_min - Error empty list
Min value in the list:  None
Method find_max_dict - Error empty dictionary
Max value in the dictionary:  None
Method find_min_dict - Error empty dictionary
Min value in the dictionary:  None
**********************************************
Calculate max and min values in empty list and empty dictionary
Method find_max - Error empty list
Max value in the list:  None
Method find_min - Error empty list
Min value in the list:  None
Method find_max_dict - Error empty dictionary
Max value in the dictionary:  None
Method find_min_dict - Error empty dictionary
Min value in the dictionary:  None
**********************************************
num_list:  [18, 20, 25, -16, 27]
temp_dict:  {'Vienna': 10, 'Paris': 11, 'Rome': 15, 'Algiers': 16}
Calculate max and min values in num_list and temp_dict
Max value in the list:  27
Min value in the list:  -16
Max value in the dictionary:  16
Method find_min_dict - Error empty dictionary
Min value in the dictionary:  None

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...