Showing posts with label Dependency Inversion Principle. Show all posts
Showing posts with label Dependency Inversion Principle. Show all posts

Friday, September 20, 2024

SOLID Principles: Dependency Inversion 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, Substitution Principle, and Interface Segregation Principle in previous articles, I will elucidate the Dependency Inversion Principle in this article with examples in Java and Python.


1. Dependency Inversion Principle (DIP)

The official definition of this principle is provided by Robert C. Martin in his book ‘Agile Software Development, Principles, Patterns, and Practices’:


- High-level modules should not depend on low-level modules. Both should depend on abstractions.

- Abstractions should not depend on details. Details should depend on abstractions.


The problem is that when high-level modules depend on low-level modules, this means that every time these low-level modules change, they force the high-level ones to change as well. High-level modules should stay abstract and independent because they contain high-level business logic and functionality of the system. Thus, the concrete modules must follow them and not the contrary. This principle aims to reverse the dependence between high-level and low-level components by abstracting from their interactions.


Let us illustrate with the help of a figure how we can implement this principle. As shown in Figure 1 below, class A (high-level class) references class B (low-level class). To implement the Dependency Inversion Principle, we decouple class A from B by adding a new interface A as an abstraction of their connection. 


Classes A and B depend now on interface A, which guarantees the abstraction between A and B. Additionally, the dependency between classes A and B reverses to become a dependency between class B and the abstraction (interface A).



We can describe the process of implementing DIP in a few steps:

- Define interfaces or abstract classes for the system functionalities.

- Make high-level modules depend on these abstractions and not on the implementations.

- Create low-level components that implement the abstractions.

- Use mechanisms such as dependency injection to allow high-level modules to create dependencies with abstractions. 


We remind that dependency injection is a paradigm that aims to decouple classes from what they depend on. If an object of a class depends on a service, we create an instance of that service and pass it as an argument in the constructor, setter methods, or other class methods. This is how we implement dependency injection.


1.1. Benefits of DIP

Using the Dependency Inversion Principle as a programming paradigm allows software components to be implemented in a highly decoupled, well-organized, and reusable manner. We can shortly summarize the advantages of DIP in terms of code aspects as follows:


Loose Coupling: This principle promotes loose coupling because high-level modules are independent of the low-level ones.


Maintainability: Adhering to this principle helps improve the system’s maintainability because changes to individual components do not affect others.


Reusability: It is possible to change the low-level components without any impact on the high-level ones that remain reusable.


Testability: This principle facilitates the testing process because it allows the replacement of the concrete components with mocks or stubs.


2. Example of DIP in Java

Let’s take a simple example of a store selling electronic devices like laptops, printers, etc. We suggest a class implementing laptop with basic properties as follows:


public class Laptop {

    int id;

    String name;

    String description;

    double price;

}


Now let’s say we have a shopping cart for a customer to purchase one or more laptops like this:


public class ShoppingCart {

    List<Laptop> laptops;

    public ShoppingCart(List<Laptop> laptops) {

        this.laptops = laptops;

    }

    public void add(Laptop lp) {

        laptops.add(lp);

    }

    public void remove(Laptop lp) {

        laptops.remove(lp);

    }

}


The ‘add’ method will be called each time a customer adds a laptop to their shopping cart. The ‘remove’ method is defined to remove a laptop from the shopping cart.


This example violates the Dependency Inversion Principle because it makes the ‘ShoppingCart’ class dependent on a low-level ‘Laptop’ class. Additionally, the store sells other products like printers, mobiles, etc. We therefore cannot extend this example to deal with these products. 


To be DIP compliant, we need to decouple ‘ShoppingCart’ from ‘Laptop’ by creating an interface as an abstraction between them. We name this interface ‘Product’:

public interface Product {

    int id = 0;

    String name = null;

    String description = null;

    double price  = 0.0;

}


Any product like a laptop or printer only needs to implement the ‘Product’ interface:

public class Laptop implements Product {

    //Add Laptop properties 

}


Now a shopping cart can contain any product and depends on the abstraction, which is the ‘Product’ interface:


public class ShoppingCart {

    List<Product> products;

    public ShoppingCart(List<Product> products) {

        this.products = products;

    }

    public void add(Product pr) {

        products.add(pr);

    }

    public void remove(Product pr) {

        products.remove(pr);

    }

}


We used dependency injection to link the high-level component (‘ShoppingCart’ class) to the ‘Product’ interface. We inject a list of the ‘Product’ interface as a parameter into the ‘ShoppingCart’ constructor. We also passed an object from the ‘Product’ interface to the ‘add’ and ‘remove’ methods.


3. Example of DIP in Python

Let’s suggest an ‘Employee’ class with some properties like ‘name’, ‘email’, and ‘salary’. The ‘Employee’ class inherits from the abstract class ‘ABC’. We define in this class ‘get_task’ as an abstract method and ‘set_salary’ and ‘get_salary’ as setter and getter for the field ‘salary’.

from abc import ABC, abstractmethod



# Abstraction

class Employee(ABC):

    def __init__(self, name, email, salary):

        self.name = name

        self.email = email

        self.salary = salary


    @abstractmethod

    def get_task(self):

        pass


    def set_salary(self, salary):

        self.salary = salary


    def get_salary(self):

        return self.salary


We create the ‘Developer’ class that inherits from the ‘Employee’ class and overrides the ‘get_task’ method.


# Low-level class

class Developer(Employee):

    def __init__(self, name, email, salary):

        super().__init__(name, email, salary)


    def get_task(self):

        print("Task1: writing code")

        print("Task2: fixing issues")


Now we define the class ‘Department’ with two attributes ‘name’ and ‘employees’. The ‘name’ property represents the department name and ‘employees’ for the list of employees belonging to the department.


The ‘department_tasks’ method iterates over each employee in the ‘employees’ list and calls the ‘get_task’ method on each employee to show tasks accomplished by him/her in the department.


A method named ‘total_salary’ calculates and returns the total salary of all employees in the ‘employees’ list. It uses a generator expression to call the ‘get_salary’ method on each employee in the list and then sum the results.


The constructor ‘__init__’ is used to initialize the ‘name’ and ‘employees’ attributes of the ‘Department’ class. Suppose we restrict the ‘employees’ list to contain only objects of the ‘Developer’ class. If we do that, we make the high-level class depending on the low-level ‘Developer’ class.


Developers are usually not the only employees in a department. A department may contain other types of employees such as managers. If we extend the ‘employee’ class with a ‘Manager’ class, this example will not work correctly and should be modified.


# High-level class

class Department:

    def __init__(self, name, employees: list[Developer]):

        self.name = name

        # Employees must be a list of developers

        for employee in employees:

            assert isinstance(employee, Developer)

        self.employees = employees


    def department_tasks(self):

        for employee in self.employees:

            employee.get_task()


    def total_salary(self):

        return sum(employee.get_salary() for employee in self.employees)


To solve such a problem, the example should adhere to DIP. Thus, the high-level class ‘Department’ must depend on the abstraction (the ‘Employee’ class) and not on an implementation like the ‘Developer’ class. We rewrite the example above and add a new class named ‘Manager’.


The ‘Manager’ class is extended with a new ‘team’ field which holds the list of employees that the manager manages. The ‘team’ attribute is initialized by the constructor ‘__init__’ of the ‘Manager’ class and can be updated by two other methods ‘add_emp_team’ and ‘remove_emp_team’.


The ‘add_emp_team’ method adds an employee to the ‘team’ list using the ‘append’ list method. The ‘remove_emp_team’ method removes an employee from the ‘team’ list using the ‘remove’ list method.


from abc import ABC, abstractmethod



# Abstraction

class Employee(ABC):

    def __init__(self, name, email, salary):

        self.name = name

        self.email = email

        self.salary = salary


    @abstractmethod

    def get_task(self):

        pass


    def set_salary(self, salary):

        self.salary = salary


    def get_salary(self):

        return self.salary



# Concrete implementation for Manager

class Manager(Employee):

    def __init__(self, name, email, salary, team: list[Employee]):

        super().__init__(name, email, salary)

        self.team = team


    def get_task(self):

        print("Task1: managing the team")

        print("Task2: managing the projects")


    def add_emp_team(self, employee):

        self.team.append(employee)


    def remove_emp_team(self, employee):

        self.team.remove(employee)


# Concrete implementation for Developer

class Developer(Employee):

    def __init__(self, name, email, salary):

        super().__init__(name, email, salary)


    def get_task(self):

        print("Task1: writing code")

        print("Task2: fixing issues")


# High-level class

class Department:

    def __init__(self, name, employees: list[Employee]):

        self.name = name

        self.employees = employees


    def department_tasks(self):

        for employee in self.employees:

            employee.get_task()


    def total_salary(self):

        return sum(employee.get_salary() for employee in self.employees)


As an example of using these classes, we define a list ‘list_employees’ composed of a manager and two developers which we inject into an instance of the ‘Department’ class.


# Creating employee instances: developers and manager

developer1 = Developer("James Stewart", "jamesstewart@gmail.com", 4500.5)

developer2 = Developer("Marie Smith", "mariesmith@gmail.com", 4355.45)

manager = Manager("Jack Doe", "jackdoe@gmail.com", 8000.90, [developer1, developer2])


list_employees = [manager, developer1, developer2]


# Injecting employees into the department

department = Department("Department of Development", list_employees)


# Displaying department work

department.department_tasks()


# Calculating total salary of all employees

total_salary = department.total_salary()

print(f"Total salary of employees [{", ".join(str(d.name) for d in list_employees)}] is: €{total_salary}")


Running the code above gives us the following output:


Task1: managing the team

Task2: managing the projects

Task1: writing code

Task2: fixing issues

Task1: writing code

Task2: fixing issues

Total salary of employees [Jack Doe, James Stewart, Marie Smith] is: €16856.85

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