Skip to main content

Solid Principles for Software Design - Part 1

The SOLID principles are five design principles that are used to make software designs more understandable, flexible, and maintainable. They are a part of many principles promoted by software engineer Robert C. Martin, author of Clean Code. Solid stands for Single Responsibility Principle (SRP), Open/Closed Principle (OCP), Liskov Substitution Principle (LSP), Interface Segregation Principle (ISP), and Dependency Inversion Principle (DIP)

In this part, we will talk about Single Responsibility Principle, Open/Closed Principle, and Liskov Substitution Principle.

1. Single Responsibility

The Single Responsibility Principle is a fundamental concept in object-oriented design that asserts that a software module, typically a class, should have one, and only one, reason to change. This principle is underpinned by two key concepts: cohesion and loose coupling.

Cohesion refers to the degree to which the elements of a module belong together. In the context of SRP, it implies that a class should encapsulate only behaviors that are closely related to its functionality. High cohesion within methods suggests that they are focused on a single task or purpose, and thus, they should be grouped within the same class.

Loose coupling describes the level of interdependence between modules. A well-designed system minimizes dependencies between classes, allowing for easier maintenance and scalability. For instance, a controller class in an API should be responsible solely for handling HTTP requests from clients and sending responses back, rather than directly implementing business logic or interacting with the database.


public class Book {
    private String title;
    private String author;
    private String content;

    public void printBook() {
        System.out.println("Printing book...");
    }

    public void save() {
        System.out.println("Saving book...");
        // Code to save the book to a database
    }
}

In the given scenario, the Book class is overburdened with multiple responsibilities. It is tasked with managing the book’s data and overseeing its printing and saving processes. This design contravenes the Single Responsibility Principle (SRP), which stipulates that a class should have only one reason to change.

To align with SRP, we can refactor the Book class by segregating its responsibilities into distinct classes. The printBook method can be extracted to a BookPrinter class, dedicated solely to the presentation of the book’s content. Similarly, the save method should be relocated to a BookRepository class, which interacts with the database and handles data persistence. This separation ensures that each class has a single, well-defined role, enhancing maintainability and promoting a cleaner, more modular architecture.


public class Book {
    private String title;
    private String author;
    private String content;
    //getter, setter methods
}

public class BookPrinter {
    public void printBook(Book book) {
        System.out.println("Printing book...");
    }
}

public class BookRepository {
    public void save(Book book) {
        System.out.println("Saving book...");
    }
}

2. Open/Closed Principles

The Open/Closed Principle (OCP) is a design concept that encourages software structures to be open for extension but closed for modification. This means that the behavior of a software component can be extended without altering its existing source code. The advantages of OCP include:

Risk Reduction: It minimizes the chance of introducing errors into an already functioning system when changes are required.

Enhanced Adaptability: By allowing new functionalities through extensions rather than modifications, the system becomes more flexible and easier to evolve over time.

Code Reusability: Extensibility inherently promotes the reuse of existing code and reduces redundancy and duplication.

Let’s examine InsurancePremiumDiscountCalculator class in Java that violates the OCP. This class is responsible for calculating insurance premiums and applying discounts.
public class InsurancePremiumDiscountCalculator {
    private String insuranceType;
    private CustomerProfile customerProfile;

    public int calculatePremiumDiscountPercentage(HealthInsuranceProfile customer) {
		if(customer.isLoyalCustomer()) {
        	return 20;
        }
        return 0;
    }
}

public class HealthInsuranceProfile {
    public boolean isLoyalCustomer() {
        return true;
    }
}
 
Imagine if in the future insurance company wants to add Vehicle insurance and then Home Insurance, we have to modify the existing InsurancePremiumDiscountCalculator class all the time and it may potentially introduce some bugs. So the solution could be refactoring the code and applying OCP principles by creating  a CustomerProfile like below:

public class InsurancePremiumDiscountCalculator {
    private String insuranceType;
    private CustomerProfile customerProfile;

    public int calculatePremiumDiscountPercentage(CustomerProfile customer) {
		if(customer.isLoyalCustomer()) {
        	return 20;
        }
        return 0;
    }
}
public interface CustomerProfile {
    public boolean isLoyalCustomer();
}

public class HealthInsuranceProfile implements CustomerProfile {
  	@Override
    public boolean isLoyalCustomer() {
        return true;
    }
}
public class VehicleInsuranceProfile implements CustomerProfile {
  	@Override
    public boolean isLoyalCustomer() {
        return true;
    }
}
 

3. Liskov Substitution Principle

The Liskov Substitution Principle (LSP) is a concept that states a subclass should be substitutable for its superclass without affecting the correctness of the program. This principle ensures that a subclass can stand in for its superclass in any situation, maintaining the expected behavior. For instance, consider a Product class and a subclass DiscountedProduct, that violates LSP:


class Product {
    protected double price;

    public void setPrice(double price) { 
    	this.price = price; 
    }
    public double getPrice() { 
    	return this.price; 
    }
}

class DiscountedProduct extends Product {
    private double discount;

    public void setDiscount(double discount) { 
    	this.discount = discount; 
    }
    public double getPrice() { 
    	return this.price - this.price * this.discount; 
    }
}
 

The DiscountedProduct class has an additional discount field, and it overrides getPrice method to return discount price. Now let's say we have a function to set a new price and then get new price.


void printNewPrice(Product product, double newPrice) {
    product.setPrice(newPrice);
    System.out.println("New price: " + product.getPrice());
}
 
If substituting a Product object with a DiscountedProduct object results in an incorrect price calculation, it indicates a breach of LSP. To comply with LSP, we can redesign the DiscountedProduct class as follows:

class DiscountedProduct extends Product {
    private double discount;

    public void setDiscount(double discount) { this.discount = discount; }
    public double getDiscountedPrice() { return this.price - this.price * this.discount; }
}
 

The necessity for such a design becomes apparent when dealing with a shopping cart system for example when calculating prices for a mix of discounted and non-discounted products. If both DiscountedProduct and hypothetical NonDiscountedProduct classes inherit from the Product class, they must implement a getPrice method. This method should function correctly, irrespective of the subclass used, ensuring that the system’s behavior remains consistent and predictable when different product types are processed.

By following LSP, we ensure that our classes are properly designed for inheritance, leading to a more robust and reliable system.


Comments

Popular posts from this blog

LINQ - Deferred Execution

Deferred Execution means that queries are not executed immediately at the time it's being created. The benefit of this is to improve performance by avoiding unnecessary executions. It also allows the query to be extendable when needed such as sorting, filtering. In the example below, the queries to retrieve courses are not being executed yet var context = new DbContext(); var courses = context.Courses      .Where(c => c.Level == 1)      .OrderBy(c => c.Name); The query is only executed when one of the following scenarios occurs: Iterating over query variable Calling ToList, ToArray, ToDictionary Calling First, Last, Single, Count, Max, Min, Average For example when we loop through the course or turn the result to a list: foreach ( var c in courses) Console.WriteLine(c.Name); OR context.Courses      .Where(c => c.Level == 1)      .OrderBy(c => c.Name).ToList();

Ensuring Data Integrity: The Role of Database Transactions

 1. What is database translation Database transactions are activities transferring, changing data from one consistent state to another consistent state for example in everyday life we make different kind of business transactions from buying products, changing or cancelling orders, buying tickets, etc. And all these activities involve the movement/transaction of data in the database. For example, consider a simple transaction of moving an amount of 5000 from one bank account to another. This transaction includes several steps: decrease the account balance by 5000, and then increase the other account balance by 50003. Each of these steps is a part of the transaction, and if any step fails, the entire transaction fails 2. Why it is crucial in reliable software systems? When it comes to business, whether it is personal or financial data, it is crucial to ensure the reliability and stability of data management. In our daily lives, we may frequently encounter errors related to database t...

Solid Principles for Software Design - Part 2

Interface Segregation Principle The Interface Segregation Principle (ISP) is one of the five SOLID principles of object-oriented design, which recommends that "Clients should not be forced to depend on interfaces they do not use". This means we should avoid implementing an interface that has unnecessary methods and therefore not going to be implemented.  Some signs of violating ISP are: Having a "fat" interface, which means having a high number of methods in one interface that are not so related to each other or low cohesion. Empty implementation of methods, certain methods of interface are not needed for implementation. Considering the following example, we violate the principle because CannonPrinter is designed only with the functionality to print, leaving the scan and fax method unimplemented. interface IMultiFunction {      void print(); void scan(); void fax(); } public class HPPrinterNScanner implements ImultiFunction { @Override public void pr...