Showing posts with label Extensibility. Show all posts
Showing posts with label Extensibility. Show all posts

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

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