Friday, September 20, 2024

Composition over Inheritance Principle



In this series, I will highlight some areas where you can improve the quality of your code during the development process. After presenting some architectural concepts like SOLID principles in previous articles, I will elucidate the ‘Inheritance over Composition Principle’ in this article. I will show how composition is implemented in Java and Python using examples.


1. Key Concepts

Inheritance and composition are object-oriented concepts used to set up relationships between classes or objects. You should use these two principles soundly because of their major effect on your software architecture. Let’s define these two mechanisms and compare them.


1.1. Inheritance

Inheritance is a fundamental concept in object-oriented programming (OOP). It is a relationship between classes, in which one class (called a sub-class) inherits properties and behaviors (fields and methods) from another class (called a super-class). Inheritance implements the ‘is-a’ relationship and draws a hierarchical relationship between classes. This concept is easy to understand and promotes code reuse.


Benefits of Inheritance

Reusability: Sub-classes reuse the fields and methods of existing super-classes.

Method Overriding: A method inherited from a super-class may not satisfy the specific behavior in a sub-class. Therefore, a sub-class can override this method and provide its own implementation.

Extensibility: Sub-classes can add new functionality to extend their super-classes.

Polymorphism: Objects are treated as instances of their super-class.


1.2. Composition

Composition is an object-oriented programming concept that defines a relationship between objects. Composition means that an object of one class (called a composite) is composed of objects of other classes (called components).


Composition implements the ‘has-a’ relationship and allows the combination of simple objects to form complex objects. Composite object methods can delegate tasks to contained object methods. This contributes to functionality reuse without inheritance.


Benefits of Composition

We’ll talk about the benefits of composition in the next section when we compare it to inheritance.


2. Inheritance over Composition Principle

The principle of composition over inheritance (or composite reuse) recommends fostering composition over inheritance for the reasons below. This doesn’t mean you never use the inheritance concept. But do it in a limited way. Overusing inheritance can lead to tight coupling and inflexible designs.


Inheritance is a powerful mechanism when you respect in your design the nature of the real system that the software represents. When you recognize an intrinsic ‘is-a’ relationship, such as a car is a vehicle, use inheritance between classes.


Preferring composition over inheritance is due to the problems induced by inheritance, such as:

Broken encapsulation: As a subclass inherits from a super-class, the sub-class can see all the details of its parent class. That’s why the concept of encapsulation is considered broken.

Large hierarchies: If you have a deep inheritance hierarchy, this will result in growing complexity and reduced clarity. When you make some changes to super-classes, it would be hard to estimate their impact on the sub-classes.

Tight coupling: Any change in the top-level super-class will lead to many changes at the sub-classes level.


On the other hand, the composition has multiple advantages like:

Flexibility: Since the component object is injected into its composite object using its interface, it is easy to override its implementation.

Reusability: Different classes can reuse the same component classes.

Loose coupling: Objects are independent of each other. Modifying one has no impact on the other. This makes the code base easier to maintain and extend.

Testability: It is better when you use composition.


3. Examples

In this section, we explain the concept of composition with the help of examples in Java and Python.

3.1. Example in Java

In this example, we show how to implement composition in Java. We define a class ‘Vehicle’, its sub-class ‘Car’, and another class ‘Steeringwheel’. An object of the ‘Steeringwheel’ class is a component of an object of the ‘Car’ class. To fulfill such a composition, we create an interface ‘ISteeringwheel’ and make the class ‘Steeringwheel’ implement this interface. 


We add a ‘steeringwheel’ attribute to the ‘Car’ class but of type ‘Isteeringwheel’ (interface) and not of type ‘Steeringwheel’ (class). The component object (‘steeringwheel’) is injected using its interface when instantiating the ‘Car’ class.


public class Vehicle { }

public interface ISteeringwheel { }

public class Steeringwheel implements ISteeringwheel{ }

public class Car extends Vehicle{

    ISteeringwheel steeringwheel;

    public Car(ISteeringwheel steeringwheel) {

        this.steeringwheel = steeringwheel;

    }

}


public class CompositionDemo {

    public static void main(String[] args) {

        ISteeringwheel steeringwheel = new Steeringwheel();

        Car car = new Car(steeringwheel);

    }

}


The advantages of using composition are obvious here. The ‘Car’ and ‘Steeringwheel’ classes are independent. Any change in the ‘Steeringwheel’ class is not visible in the ‘Car’ class and vice versa. In addition, it is easy to replace the implementation of one object of the ‘Steeringwheel’ class with another, which makes it flexible.


3.2. Example in Python

Python supports the principle of composition to create complex objects based on simpler objects. Let’s take a well-known example of a system implementing a book library to illustrate how to implement composition in Python.


This system consists of three classes ‘Library’, ‘Book’, and ‘Author’. An object of the ‘Library’ class is made up of several objects of the ‘Book’ class and each book has an author.


Author Class: Represents an author with a first name, last name, and country. The constructor of the ‘Author’ class (__init__) initializes an instance of this class with the  ‘firstname’, ‘lastname’, and ‘country’ attributes. The ‘__str__’ method displays a string representation of an object of the ‘Author’ class. This string is the result of the concatenation of its three attributes.


class Author:

    def __init__(self, firstname, lastname, country):

        self.firstname = firstname

        self.lastname = lastname

        self.country = country


    def __str__(self):

        return f'{self.firstname} {self.lastname} ({self.country})'


Book Class: Represents a book with a title, an author (instance of the ‘Author’ class), the year of publication, and the language in which the book is published.


class Book:

    def __init__(self, title, author, year, language="English"):

        self.title = title

        self.author = author

        self.year = year

        self.language = language


    def __str__(self):

        return f'"{self.title}" by {self.author} in {self.language} (published in {self.year})'


Library Class: This class has an attribute ‘books’ that stores a list of ‘Book’ objects. This class provides ‘add_book’, ‘remove_book’, and ‘show_books’ methods. The ‘add_book’ method adds a book to the library. The ‘remove_book’ method removes a book from the library. The ‘show_books’ method lists the available books in the library.


class Library:

    def __init__(self):

        self.books = []


    def add_book(self, book):

        self.books.append(book)


    def remove_book(self, book):

        self.books.remove(book)


    def show_books(self):

        for idx, book in enumerate(self.books):

            print(f'{idx + 1}. {book})')


To use this code, we can create some objects of the ‘Library’, ‘Book’, and ‘Author’ as follows:


# Creating instances of the Author class

author1 = Author("Stephen", "King", "USA")

author2 = Author("Albert", "Camus", "France")


# Creating instances of the Book class

book1 = Book("The Shining", author1, 1977, "English")

book2 = Book("The Green Mile", author1, 1996, "English")

book3 = Book("La Peste", author2, 1947, "French")


# Creating an instance of the Library class and adding books

library = Library()

library.add_book(book1)

library.add_book(book2)

library.add_book(book3)

print('''

List of books in the library after adding three books:

''')


# Listing all books in the library

library.show_books()


print('''

List of books in the library after removing one book:

''')

# Removing a book from the library

library.remove_book(book3)


# Listing all books in the library

library.show_books()


The execution of these examples gives us the following output:


List of books in the library after adding three books:


1. "The Shining" by Stephen King (USA) in English (published in 1977))

2. "The Green Mile" by Stephen King (USA) in English (published in 1996))

3. "La Peste" by Albert Camus (France) in French (published in 1947))


List of books in the library after removing one book:


1. "The Shining" by Stephen King (USA) in English (published in 1977))

2. "The Green Mile" by Stephen King (USA) in English (published in 1996))

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: 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: Liskov Substitution 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 and Open/Closed Principle in previous articles, I will elucidate the Liskov Substitution Principle in this article, with examples in Java and Python.


1. Liskov Substitution Principle

This principle states that objects in a program should be replaceable by instances of their sub-types without modifying the accuracy of this program. In other words, if S is a sub-type of T, then instances of type T may be replaced with objects of type S.


Consider an example of a program in which ‘Car’, ‘Truck’, and ‘Van’ are sub-classes of the ‘Vehicle’ class. This program adheres to LSP when each occurrence of an object of the ‘Vehicle’ class could be replaced by one of its three sub-classes without any impact on the correctness of the program.


This principle ensures that any one of the sub-classes extends its super-class without altering its behavior. That means you should only insert in the parent class properties shared by all sub-classes.


On the other hand, this principle helps you keep your software class hierarchies consistent with the Open/Closed principle. We will confirm this later with the help of an example.


We can summarize the benefits of adhering to the LSP as follows:

- It helps prevent unexpected behavior and avoid opening closed classes to make changes.

- It allows for more predictable behavior of the model hierarchy, which will be easier to extend.

- It leads to better maintenance and testability.

- It helps ensure better loose coupling between certain system components.


2. Examples of LSP in Java

To illustrate the LSP, here we take two different examples in Java language. One adheres to LSP and the second violates it.

2.1. Example 1

Let’s take a simple example of a class ‘Bird’ and suggest the following code for it:


public class Bird {


    Double weight;

    public Bird(Double weight) {

        this.weight = weight;

    }

    public void eat() {

        System.out.println("I can eat");

    }

    public void fly() {

        System.out.println("I can fly");

    }

}


An eagle is a bird and it can extend the ‘Bird’ class correctly as follows:

public class Eagle extends Bird {

    public Eagle(Double weight) {

        super(weight);

    }

    @Override

    public void eat() {

        super.eat();

        System.out.println("I am carnivore");

    }

}


But a chicken is also a bird. So, the ‘Chicken’ class can extend the ‘Bird’ class as follows:

public class Chicken extends Bird {

    public Chicken(Double weight) {

        super(weight);

    }

    @Override

    public void eat() {

        super.eat();

        System.out.println("I am omnivore");

    }

    @Override

    public void fly() {

        throw new UnsupportedOperationException("Unsupported Operation");

    }

}


But we have a problem here! The chicken cannot fly and therefore cannot support the ‘fly’ method. In this case, you need to change the behavior of the super-class, like throwing an exception, which violates the Liskov Substitution Principle.
The principal cause of this problem is that flying is not a shared property by all birds. In other words, objects of the ‘Chicken’ sub-class cannot substitute objects of the ‘Bird’ super-class. We illustrate this issue using an example. 
In the ‘BirdDemo’ class, we define ‘birdFly’ as a method that takes a list of instances of the ‘Bird’ class. It iterates over the list and calls each bird’s ‘fly’ method. In the main method of the ‘BirdDemo’ class, we create two birds as instances of the ‘Eagle’ and ‘Chicken’ classes, respectively. Then we add these two instances to a list and call the ‘birdFly’ method with this list as an argument.

import java.util.List;


public class BirdDemo {

    public static void main(String[] args) {

        Bird bird = new Eagle(10.0);

        Bird chicken = new Chicken(5.5);

        List<Bird> birdsList = List.of(bird, chicken);

        birdFly(birdsList);

    }

    public static void birdFly(List<Bird> birdsList) {

        for (Bird bird : birdsList){

            bird.fly();

        }

    }

}


Running this demonstration gives us the following result:

I can fly

Exception in thread "main" java.lang.UnsupportedOperationException: Unsupported Operation
at lsp.app.violate.Chicken.fly(Chicken.java:17)
at lsp.app.violate.BirdDemo.birdFly(BirdDemo.java:17)
at lsp.app.violate.BirdDemo.main(BirdDemo.java:12)


To fix such an issue, you need to reopen the class (violating the Open/Closed Principle) and make changes. You should account for this behavior early in the requirements phase. We can suggest a better implementation that adheres to the Liskov Substitution Principle for these classes as follows:

public class Bird {

    Double weight;

    public Bird(Double weight) {

        this.weight = weight;

    }

    public void eat() {

        System.out.println("I can eat");

    }

}


public class FlyingBird extends Bird {

    public FlyingBird(Double weight) {

        super(weight);

    }

    public void fly() {

        System.out.println("I can fly");

    }

}


public class Eagle extends FlyingBird {

    public Eagle(Double weight) {

        super(weight);

    }

    @Override

    public void eat() {

        super.eat();

        System.out.println("I am carnivore");

    }

}


public class Chicken extends Bird {

    public Chicken(Double weight) {

        super(weight);

    }

    @Override

    public void eat() {

        super.eat();

        System.out.println("I am omnivore");

    }

}


2.2. Example 2

Let’s take the famous example of ‘Shape’. Any object of the class ‘Shape’ has an area and a perimeter. For that, we suggest two methods ‘getArea()’ and ‘getPerimeter()’ to implement such behavior. However, we return something like ‘0’ in these methods because we don’t have any information that helps us perform such calculations. These details can only be available if we know what shape it is. We should create new sub-classes like ‘Circle’ and ‘Triangle’ to achieve such a goal.

public class Shape {

    protected String name;

    public Shape(String name) {

        this.name = name;

    }

    public double getArea() {

        return 0.0;

    }

    public double getPerimeter(){

        return 0.0;

    }

}


public class Shape {

    protected String name;

    public Shape(String name) {

        this.name = name;

    }

    public double getArea() {

        return 0.0;

    }

    public double getPerimeter(){

        return 0.0;

    }

}


A ‘Circle’ class inherits from the ‘Shape’ class and overrides the ‘getArea()’ and ‘getPerimeter()’ methods because we have the formula to calculate the area and perimeter of a circle. 


public class Circle extends Shape {

    private double radius;

    public Circle(String name, double radius) {

        super(name);

        this.radius = radius;

    }

    @Override

    public double getArea() {

        return Math.PI * radius * radius;

    }

    @Override

    public double getPerimeter(){

        return 2*Math.PI*radius;

    }

}


On the other hand, the ‘Triangle’ class also inherits from the ‘Shape’ class. It overrides the ‘getArea()’ and ‘getPerimeter()’ methods to calculate properly the area and the perimeter of a triangle. 



public class Triangle extends Shape {

    private double line1;

    private double line2;

    private double line3;

    public Triangle(String name, double line1, double line2, double line3) {

        super(name);

        this.line1 = line1;

        this.line2 = line2;

        this.line3 = line3;

    }

    @Override

    public double getArea() {

        double s = (line1 + line2 + line3) / 2;

        return Math.sqrt(s * (s - line1) * (s - line2) * (s - line3));

    }

    @Override

    public double getPerimeter() {

        return line1 + line2 + line3;

    }

}


The code of the ‘Shape’ class wraps the common behavior shared by all shapes. The ‘Circle’ and ‘Triangle’ classes inherit this behavior because they have an area and a perimeter. We can say that the ‘Circle’ and ‘Triangle’ instances behave exactly like an instance of the ‘Shape’ super-class. Therefore, they can be substitutable for the ‘Shape’ super-class objects. Let’s illustrate this in an example of a demo class:


public class LSPDemo {

    public static void main(String[] args) {

        Shape circle = new Circle("Circle", 16.5);

        Shape triangle = new Triangle("Triangle", 10.0, 14.0, 18.0);

        List<Shape> shapesList = List.of(circle, triangle);

        double shapesTotalArea = getShapesTotalArea(shapesList);

        System.out.println("Shapes total area: " + shapesTotalArea);

    }


    public static double getShapesTotalArea(List<Shape> shapesList) {

        double shapesTotalArea = 

shapesList.stream().map(s->s.getArea()).reduce(Double::sum).get();

        return shapesTotalArea;

    }

}


In this example, we create two shapes as instances of the ‘Circle’ and ‘Triangle’ classes.  We put these two instances in the ‘shapesList’ list which does not distinguish between circles and triangles. 
We define a function ‘getShapesTotalArea’ which takes a list of objects of the ‘Shape’ class and calculates the total area of all shapes in the list. It uses the Java Stream API to map each ‘Shape’ object to its area using its ‘getArea’ method. Then, it sums them up and returns the total area.
This implementation adheres to the Liskov Substitution Principle. Objects of the ‘Circle’ and ‘Triangle’ sub-classes have successfully replaced objects of the ‘Shape’ super-class in the ‘getShapesTotalArea’ function. Executing the main function of the ‘LSPDemo’ class gives us the following result:


Shapes total area: 924.9477205372846


3. Example of LSP in Python

Suppose we have a class hierarchy that represents different species of animals. We have a base class ‘Animal’ and two sub-classes ‘Dog’ and ‘Fish’. To adhere to LSP, instances of ‘Dog’ and ‘Fish’ should be able to replace instances of ‘Animal’ without affecting the correctness of the program. Let’s inspect if this is the case in our example.


The base class ‘Animal’ has a ‘walk’ method supported by its sub-class ‘Dog’ but not by its sub-class ‘Fish’. In our example, we threw an exception to indicate that this operation is unsupported in the ‘Fish’ sub-class. In this case, an instance of the ‘Fish’ sub-class cannot substitute an object of the ‘Animal’ super-class. Thus, this example violates the Liskov Substitution Principle. 


To illustrate that, we define a function ‘animal_walking’ that takes an animal object as input and calls its ‘walk’ method. As fishes are animals but cannot walk, then the ‘walk’ method should not belong to the ‘Animal’ class but to a sub-class in the hierarchy to regroup only animals that have this feature.


class Animal:

    def __init__(self, weight):

        self.weight = weight

    def eat(self):

        print("I can eat")

    def walk(self):

        print("I can walk")


class Dog(Animal):

    def __init__(self, weight, no_legs):

        super().__init__(weight)

        self.no_legs = no_legs

    def bark(self):

        print("I can bark")


class Fish(Animal):

    def walk(self):

        raise Exception("Sorry, unsupported operation")

    def swim(self):

        print("I can swim")


def animal_walking(animal):

    animal.walk()



# Examples of using these classes

dog = Dog(10, 4)

animal_walking(dog)

fish = Fish(2)



# Violation of Liskov Substitution Principle

animal_walking(fish)


Running the above example gives us the following output:


I can walk

Traceback (most recent call last):

...

    raise Exception("Sorry, unsupported operation")

Exception: Sorry, unsupported operation

SOLID Principles: Open/Closed 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 in the previous article, I will elucidate the Open/Closed Principle in this article, with examples in Java and Python.


1. Open/Closed Principle (OCP)

This principle means that software entities such as modules, classes, or functions should be open to extension but closed to modification. In other words, it is allowed to extend the behavior of these entities but it is prohibited to modify their source code. The reasons behind this principle are:


- Existing entities are already well-tested. Therefore, any modification could lead to bugs and unexpected behavior. It is better to avoid changing what already works well as much as possible. You can extend it whenever you need instead of modifying it.

- Testers do not need to test the entire flow, just the added parts.

- Maintenance remains easy.

- Single Responsibility Principle will be respected.


2. Example of OCP in Java

To see this principle in action, consider a well-known example that calculates the total price of a shopping cart. As a shopping cart contains products, we create an interface named ‘Product’ with a single ‘getPrice()’ method. Next, we suggest the ‘Item’ class to implement this interface. This class has three fields ‘name’, ‘description’, and ‘price’. We have overridden the ‘getPrice()’ method to return the price value.


public interface Product {

    double getPrice();

}


public class Item implements Product {

    private final String name;

    private final String description;

    private final double price;


    public Item(String name, String description, double price) {

        this.name = name;

        this.description = description;

        this.price = price;

    }


    @Override

    public double getPrice() {

        return price;

    }

}


We can now create the ‘ShoppingCart’ class with some fields like the currency used and the list of products to purchase. These two fields must be initialized at the time of instantiation of the class. We need two methods to fulfill the functionality of a shopping cart, ‘addProduct’ and ‘getTotalPrice’. The ‘addProduct’ method allows you to add a product to the shopping cart.


The ‘getTotalPrice()’ method calculates the total price of a collection of objects of the ‘Item’ class in a shopping cart. If the list containing the items is empty, the method returns 0.


If the products list is not empty, it maps each item within the list to its price, then reduces the stream to a single value (the sum of all prices) using Double::sum. Finally, it uses the ‘round’ method to round the calculated total price to two decimal places.


public class ShoppingCart {

    protected final String currency;

    protected final List<Product> products;

    public ShoppingCart(String currency, List<Product> products) {

        this.currency = currency;

        this.products = products;

    }

    public String getCurrency() {

        return currency;

    }


    public void addProduct(Product product) {

        products.add(product);

    }


    public double getTotalPrice() {

        if (products.isEmpty()) return 0;

        //Calculate the total price

        double totalPrice = products.stream()

.map(product -> product.getPrice()).reduce(Double::sum).get();

        //Round the price to 2 decimal places

        return round(totalPrice, 2);

    }

}


It is common to apply discounts on products. But, from an architectural point of view, it is not a good idea to be applied directly to the ‘ShoppingCart’ but to its sub-class. Here the adherence to the Open/Closed Principle comes into play. We do not open the ‘ShoppingCart’ class to add this behavior, but we extend it with a new sub-class to implement the new feature.


Now let’s create a new class called ‘DiscountedShoppingCart’ to implement the new behavior of applying discounts to specific items based on certain criteria. This class has two fields ‘priceLimit’ and ‘discount’ which need to be initialized during the class instantiation. The ‘priceLimit’ property determines the maximum price of a product, which must not be exceeded to apply a discount. The ‘discount’ property is the percentage of the discount.


The ‘DiscountedShoppingCart’ class will calculate the total price differently than its super-class. The sub-class will take into consideration the discount rule. Therefore, ‘DiscountedShoppingCart’ will replace the ‘getTotalPrice’ method with a new behavior.


The ‘getTotalPrice’ method calculates the total price of a shopping cart with a discount applied when certain conditions are met. It first calls the ‘getTotalPrice’ method of the super-class to get the base total price. It returns the base total price if the list of products in the shopping cart is empty. Otherwise, it calculates a discount based on the price of each product and a price limit (‘priceLimit’). It applies a discount rate (discount) to the price of each product if it is less than or equal to the ‘priceLimit’ value.

The discount obtained is subtracted from the base total price. The final total price is rounded to 2 decimal places using the ‘round’ and returned as a result.


public class DiscountedShoppingCart extends ShoppingCart {

    private final double priceLimit;

    private final double discount;

    public DiscountedShoppingCart(String currency, List<Product> products, 

double priceLimit, double discount) {

        super(currency, products);

        this.priceLimit = priceLimit;

        this.discount = discount;

    }


    @Override

    public double getTotalPrice() {

        double totalPrice = super.getTotalPrice();

        if (products.isEmpty()) return totalPrice;

        //Calculate the discount

        double discount = products.stream()

.filter(p->p.getPrice()<=this.priceLimit&&this.priceLimit>0)

                              .map(p->p.getPrice()*this.discount).reduce(Double::sum).get();

        totalPrice -= discount;

        //Round the price to 2 decimal places

        return round(totalPrice, 2);

    }

}


We suggest the ‘ShoppingCartDemo’ class, a simple demonstration of a shopping cart system. We create three items, put them in a shopping cart, and then print out the total price of the shopping cart in euros. We then create a discounted shopping cart, add the same items, and print the total discounted price. Finally, we print out the discounted total price.


public class ShoppingCartDemo {

    public static void main(String[] args) {

        //Create some items

        Product item1 = new Item("Shampoo", "Herbal essences, 350ml for Dry hair", 10.0);

        Product item2 = new Item("Cream", "Nivea Soft, 100ml", 31.99);

        Product item3 = new Item("Tablet", "iPad 6 (2020) | 9.7", 149.99);

        //Create a shopping cart

        ShoppingCart shoppingCart = new ShoppingCart("EUR", new ArrayList<>());

        shoppingCart.addProduct(item1);

        shoppingCart.addProduct(item2);

        shoppingCart.addProduct(item3);

        System.out.println("Total price: " + shoppingCart.getTotalPrice() 

+ " " + shoppingCart.getCurrency());

        //Create a discounted shopping cart

        DiscountedShoppingCart discShoppingCart = 

new DiscountedShoppingCart("EUR", new ArrayList<>(), 50.0, 0.25);

        discShoppingCart.addProduct(item1);

        discShoppingCart.addProduct(item2);

        discShoppingCart.addProduct(item3);


        System.out.println("Discounted Total price: " + discShoppingCart.getTotalPrice() 

+ " " + discShoppingCart.getCurrency());

    }

}


The execution of this code gives us the following results:


Total price: 191.98 EUR

Discounted Total price: 181.48 EUR


In summary, the ‘ShoppingCart’ class is closed for modification and the ‘DiscountedShoppingCart’ class is open for extension to add new functionality to apply discounts to certain products, without modifying the existing ‘ShoppingCart’ class.


3. Example of OCP in Python

We can demonstrate the open/closed principle in Python using a famous example of the ‘Account’ and ‘SaveAccount’ classes. A simple implementation of the ‘Account’ class can have two properties, ‘acc_num’ for the account name and the ‘balance’ property for the amount of money available.


Two methods are necessary for each account, ‘deposit’ and ‘withdraw’. The ‘deposit’ method consists of saving money in the account. The ‘withdraw’ method involves withdrawing money from the account. To show the account number and balance, we define the ‘show_state’ method.


class Account:

    def __init__(self, acc_num, balance):

        self.acc_num = acc_num

        self.balance = balance


    def deposit(self, amount):

        self.balance += amount


    def withdraw(self, amount):

        if amount <= self.balance:

            self.balance -= amount

        else:

            print("Your balance is not enough.")


    def show_state(self):

        return f"Account number: {self.acc_num}, Balance: {self.balance}"


To keep the ‘Account’ class representing the base account that encapsulates the common behavior of all accounts, we need to keep it closed and avoid any changes that might disrupt this correct functionality. But bank customers need other services, like saving money for a long time and earning interest. To do this, we extend our ‘Account’ class with a new class to complete this new feature.


Let’s create the ‘SaveAccount’ class that inherits from the ‘Account’ class and adds an interest rate property. We define a new method with the name ‘add_interest’ to calculate interest based on the balance and interest rate and add it to the balance.


We can say that this example adheres to the Open/Closed Principle because it extends the system with the ‘SaveAccount’ class without modifying the existing ‘Account’ class.


class SaveAccount(Account):

    def __init__(self, acc_num, balance, interest_rate):

        super().__init__(acc_num, balance)

        self.interest_rate = interest_rate


    def add_interest(self):

        interest = self.balance * self.interest_rate / 100

        self.balance += interest


    def show_state(self):

        return f"Saved " + super().show_state() + f", Interest Rate: {self.interest_rate}%"


# Some example operations on account

acc = Account("A952 7693 4621 7834", 7500.0)

print(acc.show_state())

acc.withdraw(350)

print(acc.show_state())

acc.deposit(1500.5)

print(acc.show_state())


print('')


# Some example of operations on save account

save_acc = SaveAccount("A5793 7693 4621 7834", 5700.0, 2.5)

print(save_acc.show_state())

save_acc.add_interest()

print(save_acc.show_state())


Running the above code gives us the following result:


Account number: A952 7693 4621 7834, Balance: 7500.0

Account number: A952 7693 4621 7834, Balance: 7150.0

Account number: A952 7693 4621 7834, Balance: 8650.5


Saved Account number: A5793 7693 4621 7834, Balance: 5700.0, Interest Rate: 2.5%

Saved Account number: A5793 7693 4621 7834, Balance: 5842.5, Interest Rate: 2.5%

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

Roadmap to Improve Code Quality

 


In this series, I will highlight some areas where you can improve the quality of your code during the development process. In this first article, I explain why code quality is paramount. Then I will briefly introduce you to some ideas that will 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 the next 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.


1. Why Code Quality is Paramount ?

Code quality is the final criterion for shipping and using a product. I believe that high-quality code is the key to the success of any IT business. If the product code is well structured, readable, and extensible, making changes or adding new features is much easier. This saves you a lot of time and ensures the satisfaction of your customers.


High-quality code contributes to effective collaboration between developers. Well-documented, readable code helps team members understand each other’s work, which increases their productivity. On the other hand, if the code is of poor quality (e.g. unreadable), a new developer will invest more time and energy than usual in understanding the code to make changes to it. High-quality code is easy to understand and requires low development effort.


Another costly problem you may encounter is technical debt. This is the supplementary cost of additional work resulting from choosing a quick fix for quick delivery rather than working on a high-quality product that would take longer. This means that if you postpone the measures that contribute to increasing code quality, the development time of your software will slow down in the medium and long term.


2. What to Do to Improve Code Quality

To improve the quality of your code, the solution to follow is usually a mixture of good architecture based on long-discussed requirements analysis, technical rules, best practices, and refined processes, which we would like to briefly present here:


- Use software architecture models, design patterns, and SOLID principles to produce a code of high-quality and boost your object-oriented thinking and modeling.


- Respect programming concepts such as adherence to a programming language’s recommended coding standard, modularity, and loose coupling to improve code quality aspects like readability and extensibility.


- Follow standard rules and recommendations such as CERT coding standards to eliminate code problems like security issues.


- Use tools such as static analysis tools to solve multiple types of code problems like cognitive complexity.


- Take full advantage of the multiple options that your IDE (Integrated Development Editor) offers, such as the ability to analyze your code or shortcuts to speed up your coding.


- Use logging extensively, which is a good mechanism to track what happens during the execution of an application in a production environment.


- Adhere to processes such as unit testing and debugging. They help identify what went wrong and whether your code meets its requirements. They therefore play a significant role in enhancing the quality of your code.


- Set up a CI/CD (Continuous Integration and Deployment) pipeline to automate the build, test, and deployment processes. Automate all possible tasks in the CI/CD pipeline, such as testing, code analysis, and other quality checking.

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