top of page
  • Writer's pictureRahul R

SOLID Principles in Object Oriented Design

Solid Object-Oriented Design, commonly known as SOLID, is a set of principles for object-oriented programming that helps developers create software that is easy to maintain, scalable, and extensible. The SOLID principles were introduced by Robert C. Martin (a.k.a Uncle Bob) in the early 2000s and have since become a cornerstone of object-oriented design. In this blog, we will explore the five SOLID principles and how they can be applied to design robust and scalable object-oriented software.

  1. Single Responsibility Principle (SRP) The Single Responsibility Principle states that a class should have only one reason to change. This means that a class should have only one responsibility or job to do. By following this principle, classes become more focused and easier to maintain. When a class has multiple responsibilities, changes to one responsibility can break other parts of the class, making it more difficult to maintain.

  2. Open-Closed Principle (OCP) The Open-Closed Principle states that a class should be open for extension but closed for modification. This means that a class should be designed in such a way that it can be extended without modifying its existing code. This principle encourages the use of abstraction and inheritance to create flexible and scalable software.

  3. Liskov Substitution Principle (LSP) The Liskov Substitution Principle states that any object of a parent class should be able to be replaced with an object of a child class without affecting the correctness of the program. In simpler terms, this means that derived classes should be substitutable for their base classes without causing any problems.

  4. Interface Segregation Principle (ISP) The Interface Segregation Principle states that a class should not be forced to depend on interfaces it does not use. In other words, it is better to have several small, specific interfaces than a single large, general-purpose interface. This principle helps to reduce the dependencies between classes, making the system more modular and easier to maintain.

  5. Dependency Inversion Principle (DIP) The Dependency Inversion Principle states that high-level modules should not depend on low-level modules, but both should depend on abstractions. This principle encourages the use of interfaces to decouple the implementation of classes from their usage. By doing so, the system becomes more flexible and adaptable to change.

Single Responsibility Principle


To further illustrate the Single Responsibility Principle (SRP), let's take a look at an example of a class that violates the SRP and how it can be refactored to adhere to the SRP.

Consider a class called Customer that represents a customer in an e-commerce system. The Customer class has the following responsibilities:


  1. Store the customer's personal information (name, email, address, etc.).

  2. Handle the customer's orders (create, modify, cancel, etc.).

  3. Send email notifications to the customer.


This violates the SRP because the Customer class has multiple responsibilities. To adhere to the SRP, we can refactor the Customer class by separating out its responsibilities into separate classes.

First, we create a CustomerInfo class that is responsible for storing the customer's personal information:


class CustomerInfo:

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

self.name = name

self.email = email

self.address = address


Next, we create an OrderManager class that is responsible for handling the customer's orders:


class OrderManager:

def create_order(self, customer, items):

# create order

pass

def modify_order(self, customer, order_id, new_items):

# modify order

pass

def cancel_order(self, customer, order_id):

# cancel order

pass


Finally, we create an EmailNotifier class that is responsible for sending email notifications to the customer:


class EmailNotifier:

def send_email(self, customer, subject, message):

# send email

pass


With these three classes, we have separated out the responsibilities of the Customer class into separate classes that adhere to the SRP. The Customer class now only has one responsibility, which is to coordinate the interactions between the CustomerInfo, OrderManager, and EmailNotifier classes:


class Customer:

def __init__(self, customer_info, order_manager, email_notifier):

self.customer_info = customer_info

self.order_manager = order_manager

self.email_notifier = email_notifier

def create_order(self, items):

self.order_manager.create_order(self, items)

def modify_order(self, order_id, new_items):

self.order_manager.modify_order(self, order_id, new_items)

def cancel_order(self, order_id):

self.order_manager.cancel_order(self, order_id)

def send_email(self, subject, message):

self.email_notifier.send_email(self, subject, message)


In summary, the SRP is a principle in object-oriented design that states that a class should have only one responsibility. By separating out the responsibilities of a class into separate classes, code becomes easier to understand, modify, and test.


The Open-Closed Principle (OCP) is a principle in object-oriented programming that states that software entities (classes, modules, functions, etc.) should be open for extension but closed for modification. This means that a software component should be easily extendable without requiring modification of the existing code.

Let's take a look at an example of how the OCP can be applied in practice. Suppose we have a software system that needs to calculate the total price of a shopping cart. We have a base class Cart that defines the interface for our shopping cart:


class Cart:

def __init__(self):

self.items = []

def add_item(self, item):

self.items.append(item)

def remove_item(self, item):

self.items.remove(item)

def get_total_price(self):

total = 0

for item in self.items:

total += item.get_price()

return total


In this example, Cart has a single responsibility, which is to manage the items in the shopping cart and calculate the total price. However, this implementation violates the OCP because it is not closed for modification. If we want to add a new type of item to our shopping cart, we will need to modify the Cart class by adding a new if statement to the get_total_price method:


class Cart:

def __init__(self):

self.items = []

def add_item(self, item):

self.items.append(item)

def remove_item(self, item):

self.items.remove(item)

def get_total_price(self):

total = 0

for item in self.items:

if isinstance(item, Book):

total += item.get_price() * 0.9

else:

total += item.get_price()

return total


This implementation violates the OCP because we have modified the existing code to accommodate the new requirement. Instead, we should follow the OCP by creating a new class DiscountedItem that extends Item and implements the get_discounted_price method:


class Item:

def __init__(self, price):

self.price = price

def get_price(self):

return self.price

class DiscountedItem(Item):

def get_discounted_price(self):

return self.price * 0.9

class Book(DiscountedItem):

pass

class Cart:

def __init__(self):

self.items = []

def add_item(self, item):

self.items.append(item)

def remove_item(self, item):

self.items.remove(item)

def get_total_price(self):

total = 0

for item in self.items:

if isinstance(item, DiscountedItem):

total += item.get_discounted_price()

else:

total += item.get_price()

return total


In this implementation, we have followed the OCP by creating a new class DiscountedItem that extends Item and implements the get_discounted_price method. This allows us to add new types of items to our shopping cart without modifying the existing code.

In conclusion, the Open-Closed Principle is an important principle in object-oriented programming that can help us create software systems that are easy to maintain and extend. By following the OCP, we can create software components that are open for extension but closed for modification.


The Liskov Substitution Principle


The Liskov Substitution Principle (LSP) is a fundamental principle of object-oriented programming that states that objects of a superclass should be able to be replaced with objects of a subclass without altering the correctness of the program. In other words, a subclass should be able to substitute for its superclass without causing errors or unexpected behavior.

Let's take an example of a shopping cart to understand LSP in Python. We have a ShoppingCart class which has a method calculate_total() that calculates the total cost of items in the cart. We also have two subclasses Product and DiscountedProduct that inherit from the Product class. The DiscountedProduct class has a discount percentage applied to its price.


class ShoppingCart:

def __init__(self):

self.products = []


def add_product(self, product):

self.products.append(product)


def calculate_total(self):

total = 0

for product in self.products:

total += product.price

return total


class Product:

def __init__(self, price):

self.price = price


class DiscountedProduct(Product):

def __init__(self, price, discount_percentage):

super().__init__(price)

self.discount_percentage = discount_percentage


@property

def price(self):

return super().price * (1 - self.discount_percentage)


Now let's imagine we have a client who wants to use our ShoppingCart class to calculate the total cost of their products. They have a mixture of Product and DiscountedProduct objects in their cart. They expect the calculate_total() method to accurately calculate the total cost of their items.

Since DiscountedProduct is a subclass of Product, we should be able to substitute a DiscountedProduct object for a Product object without altering the behavior of the program. This means that when we call calculate_total() on our shopping cart, the total should accurately reflect the discounts applied to the DiscountedProduct objects.


shopping_cart = ShoppingCart()


# add products to the cart

product1 = Product(10)

product2 = DiscountedProduct(20, 0.1)

shopping_cart.add_product(product1)

shopping_cart.add_product(product2)


# calculate total cost

total = shopping_cart.calculate_total()


print(total) # expected output: 27


In the above example, we have successfully applied the Liskov Substitution Principle. We were able to substitute a DiscountedProduct object for a Product object without altering the behavior of the program. The calculate_total() method accurately calculated the total cost of the items in the shopping cart, including the discounts applied to the DiscountedProduct object.


The Interface Segregation Principle


The Interface Segregation Principle (ISP) is one of the SOLID design principles in object-oriented programming. It states that clients should not be forced to depend on interfaces they do not use. In other words, it's better to have multiple small, specialized interfaces rather than one large, general-purpose interface.

Let's take a look at an example of how ISP can be applied to a shopping cart program in Python.

Suppose we have a ShoppingCart class that has several methods such as add_item(), remove_item(), calculate_total(), apply_discount(), and checkout(). However, not all clients of the ShoppingCart class need all of these methods. Some clients only need to add and remove items, while others only need to calculate the total and apply discounts.

To apply the ISP, we can break down the ShoppingCart interface into smaller, more specialized interfaces that clients can use as needed. For example, we can create an AddRemoveItems interface with add_item() and remove_item() methods, and a CalculateDiscount interface with calculate_total() and apply_discount() methods. Clients can then implement only the interfaces they need.

Here's an example implementation in Python:


from abc import ABC, abstractmethod


class AddRemoveItems(ABC):

@abstractmethod

def add_item(self, item):

pass

@abstractmethod

def remove_item(self, item):

pass


class CalculateDiscount(ABC):

@abstractmethod

def calculate_total(self):

pass

@abstractmethod

def apply_discount(self, discount):

pass


class ShoppingCart(AddRemoveItems, CalculateDiscount):

def __init__(self):

self.items = []

self.total = 0

def add_item(self, item):

self.items.append(item)

def remove_item(self, item):

self.items.remove(item)

def calculate_total(self):

for item in self.items:

self.total += item.price

return self.total

def apply_discount(self, discount):

self.total -= self.total * discount


In this example, we define the AddRemoveItems and CalculateDiscount interfaces using abstract methods in Python's abc module. Then, we have the ShoppingCart class implement both interfaces as needed.

Clients can now choose to depend on either AddRemoveItems or CalculateDiscount, or both, depending on their needs. This allows for more flexibility and reduces the likelihood of unintended consequences when implementing or modifying the ShoppingCart class.

By applying the ISP, we have created a more cohesive and maintainable design for our shopping cart program.


The Dependency Inversion Principle


The Dependency Inversion Principle (DIP) is a software design principle that suggests that 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. This helps in decoupling the system and promoting flexibility.

Let's take an example of a shopping cart to understand the Dependency Inversion Principle.

Suppose we have a Cart class which is responsible for managing the items added to the cart and their quantities. It has a total method which returns the total cost of all the items in the cart.


class Cart:

def __init__(self):

self.items = {}


def add_item(self, item, quantity):

if item in self.items:

self.items[item] += quantity

else:

self.items[item] = quantity


def remove_item(self, item, quantity):

if item in self.items:

if self.items[item] >= quantity:

self.items[item] -= quantity

if self.items[item] == 0:

del self.items[item]


def total(self):

total = 0

for item, quantity in self.items.items():

total += item.price * quantity

return total


The Cart class directly depends on the Item class, which is a low-level detail. If we decide to change the way items are priced, for example, by adding taxes or discounts, we would have to modify the Cart class.

To adhere to the Dependency Inversion Principle, we can create an interface PricedItem that declares a price method. Then we can make the Item class implement the PricedItem interface.


class PricedItem:

def price(self):

pass


class Item(PricedItem):

def __init__(self, name, price):

self.name = name

self.price = price


def price(self):

return self.price


Now, instead of directly depending on the Item class, the Cart class depends on the PricedItem interface. This allows us to change the way items are priced without modifying the Cart class.


class Cart:

def __init__(self):

self.items = {}


def add_item(self, item, quantity):

if item in self.items:

self.items[item] += quantity

else:

self.items[item] = quantity


def remove_item(self, item, quantity):

if item in self.items:

if self.items[item] >= quantity:

self.items[item] -= quantity

if self.items[item] == 0:

del self.items[item]


def total(self):

total = 0

for item, quantity in self.items.items():

total += item.price() * quantity

return total


By using the Dependency Inversion Principle, we have created a more flexible and maintainable system that promotes loose coupling between modules.


In conclusion, by following the SOLID principles, developers can design software that is more modular, flexible, and scalable. The SOLID principles help to reduce dependencies between classes, making the system easier to maintain and extend. It is important to remember that these principles are not rules set in stone, but rather guidelines that should be adapted to fit the needs of the specific project. By applying SOLID principles to your object-oriented design, you can create software that is robust, maintainable, and adaptable to future changes.


34 views0 comments

Recent Posts

See All

How to setup reverse proxy on apache2

To set up a reverse proxy to a local installation using Apache2, you need to configure the Apache Virtual Hosts to forward requests to your local server. Here's a step-by-step guide: Enable Proxy Modu

How to Set Up Odoo with Apache Reverse Proxy on Ubuntu

Welcome, adventurous souls, to the whimsical world of Odoo installation! In this zany tutorial, we'll embark on a wild journey to set up Odoo, the beloved open-source ERP software, with an Apache reve

How to Attach S3 to your Django Project

Install Required Packages: Install django-storages package, which provides support for various storage backends including Amazon S3. You can install it via pip: pip install django-storages Configure D

Comments


bottom of page