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

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