SOLID Principles Clean Code & Object Oriented Design

Zaheer Ahmad 5 min read min read
Python
SOLID Principles Clean Code & Object Oriented Design

Introduction

The SOLID principles are a set of five design rules that help developers write clean, maintainable, and scalable code in object-oriented programming (OOP). These principles are widely used in professional software development and are essential for mastering clean code and object oriented design principles.

For Pakistani students studying computer science in universities like those in Lahore, Karachi, and Islamabad, learning SOLID principles can significantly improve coding skills and job readiness. Whether you're building a university project, a freelancing application, or preparing for software engineering interviews, SOLID helps you write code that is easier to understand, modify, and extend.

In simple terms, SOLID principles help you:

  • Avoid messy, hard-to-maintain code
  • Build flexible and reusable software
  • Work better in team-based development environments

Prerequisites

Before starting this solid principles tutorial, you should have:

  • Basic understanding of Object-Oriented Programming (OOP)
  • Familiarity with any programming language (Python, Java, or C# preferred)
  • Knowledge of:
    • Classes and objects
    • Inheritance
    • Functions/methods

Core Concepts & Explanation

Single Responsibility Principle (SRP)

Definition: A class should have only one reason to change.

This means a class should perform only one task or responsibility.

Bad Example (SRP Violation):

class Student:
    def calculate_fee(self):
        return 50000

    def print_receipt(self):
        print("Fee receipt printed")

Problem:
The Student class is handling both business logic (fee calculation) and presentation logic (printing receipt).

Good Example (SRP Applied):

class Student:
    def calculate_fee(self):
        return 50000

class ReceiptPrinter:
    def print_receipt(self):
        print("Fee receipt printed")

Explanation:

  • Student class handles only student-related logic
  • ReceiptPrinter handles printing
  • Each class has a single responsibility

Open/Closed Principle (OCP)

Definition: Software should be open for extension but closed for modification.

You should be able to add new functionality without changing existing code.

Example:

class Discount:
    def get_discount(self, customer_type):
        if customer_type == "student":
            return 10
        elif customer_type == "teacher":
            return 20

Problem:
If a new type (e.g., "freelancer") is added, you must modify this class.

Better Approach:

class Discount:
    def get_discount(self):
        pass

class StudentDiscount(Discount):
    def get_discount(self):
        return 10

class TeacherDiscount(Discount):
    def get_discount(self):
        return 20

Explanation:

  • New discount types can be added without modifying existing classes
  • Code is easier to extend

Liskov Substitution Principle (LSP)

Definition: Subclasses should be replaceable with their parent class without breaking functionality.

Bad Example:

class Bird:
    def fly(self):
        print("Flying")

class Penguin(Bird):
    def fly(self):
        raise Exception("Penguins can't fly")

Problem:
Penguin breaks the expected behavior of Bird.

Better Approach:

class Bird:
    pass

class FlyingBird(Bird):
    def fly(self):
        print("Flying")

class Penguin(Bird):
    def swim(self):
        print("Swimming")

Explanation:

  • Only birds that can fly inherit from FlyingBird
  • No unexpected behavior

Interface Segregation Principle (ISP)

Definition: Clients should not be forced to depend on methods they do not use.

Bad Example:

class Worker:
    def work(self):
        pass

    def eat(self):
        pass

Problem:
A robot worker does not need eat().

Better Approach:

class Workable:
    def work(self):
        pass

class Eatable:
    def eat(self):
        pass

Explanation:

  • Separate interfaces for different responsibilities
  • Classes implement only what they need

Dependency Inversion Principle (DIP)

Definition: High-level modules should not depend on low-level modules. Both should depend on abstractions.

Bad Example:

class MySQLDatabase:
    def connect(self):
        print("Connected to MySQL")

class App:
    def __init__(self):
        self.db = MySQLDatabase()

Problem:
App is tightly coupled to MySQLDatabase.

Better Approach:

class Database:
    def connect(self):
        pass

class MySQLDatabase(Database):
    def connect(self):
        print("Connected to MySQL")

class App:
    def __init__(self, db: Database):
        self.db = db

Explanation:

  • App depends on abstraction (Database)
  • You can switch to PostgreSQL easily

Practical Code Examples

Example 1: Student Fee Management System

class FeeCalculator:
    def calculate_fee(self, base_fee):
        return base_fee + 5000  # additional charges

class Discount:
    def apply_discount(self, fee):
        return fee * 0.9  # 10% discount

class Receipt:
    def generate(self, fee):
        print(f"Total Fee: {fee} PKR")

# Usage
calculator = FeeCalculator()
discount = Discount()
receipt = Receipt()

fee = calculator.calculate_fee(50000)
discounted_fee = discount.apply_discount(fee)
receipt.generate(discounted_fee)

Line-by-line Explanation:

  • FeeCalculator: Calculates student fee
  • Discount: Applies discount logic
  • Receipt: Handles output
  • Objects are created separately → follows SRP
  • Code is modular and reusable

Example 2: Real-World Application (E-Commerce in Pakistan)

Imagine Ahmad runs an online store in Karachi.

class PaymentMethod:
    def pay(self, amount):
        pass

class JazzCash(PaymentMethod):
    def pay(self, amount):
        print(f"Paid {amount} PKR via JazzCash")

class EasyPaisa(PaymentMethod):
    def pay(self, amount):
        print(f"Paid {amount} PKR via EasyPaisa")

class Order:
    def __init__(self, payment_method: PaymentMethod):
        self.payment_method = payment_method

    def checkout(self, amount):
        self.payment_method.pay(amount)

# Usage
order = Order(JazzCash())
order.checkout(2000)

Line-by-line Explanation:

  • PaymentMethod: Abstract class
  • JazzCash and EasyPaisa: Concrete implementations
  • Order: Depends on abstraction → DIP
  • Easy to add new payment methods (OCP)

Common Mistakes & How to Avoid Them

Mistake 1: Creating God Classes

Problem: One class does everything.

Fix:

# Split responsibilities into multiple classes

Tip: Always ask: “Does this class have more than one responsibility?”


Mistake 2: Overusing Inheritance

Problem: Deep inheritance hierarchy makes code complex.

Fix: Use composition instead.

class Engine:
    def start(self):
        print("Engine started")

class Car:
    def __init__(self):
        self.engine = Engine()

Explanation:

  • Car uses Engine instead of inheriting from it

Practice Exercises

Exercise 1: Refactor Student Class

Problem:
A class handles both student data and printing.

Solution:

class Student:
    def get_name(self):
        return "Ali"

class Printer:
    def print_name(self, name):
        print(name)

Explanation:

  • Responsibilities separated → SRP applied

Exercise 2: Add New Payment Method

Problem: Add “Bank Transfer” without modifying existing code.

Solution:

class BankTransfer(PaymentMethod):
    def pay(self, amount):
        print(f"Paid {amount} PKR via Bank Transfer")

Explanation:

  • New functionality added without changing old code → OCP

Frequently Asked Questions

What is SOLID in programming?

SOLID is a set of five object-oriented design principles that help developers write clean, maintainable, and scalable code. These principles improve code structure and flexibility.

Why are SOLID principles important?

They make code easier to understand, test, and extend. In real-world software development, especially in team projects, SOLID principles reduce bugs and improve collaboration.

How do I apply SOLID principles in projects?

Start by designing small classes with single responsibilities, use interfaces or abstractions, and avoid tightly coupled code. Practice regularly with real-world examples.

Are SOLID principles only for large projects?

No, they are useful for both small and large projects. Even in university assignments, applying SOLID principles improves code quality.

Which programming languages support SOLID?

SOLID principles can be applied in any object-oriented language such as Python, Java, C++, and C#.


Summary & Key Takeaways

  • SOLID principles improve clean code and maintainability
  • Each class should have a single responsibility (SRP)
  • Code should be extendable without modification (OCP)
  • Subclasses should behave like their parent classes (LSP)
  • Avoid forcing classes to implement unnecessary methods (ISP)
  • Use abstractions to reduce dependency (DIP)

To deepen your understanding, explore these tutorials on theiqra.edu.pk:

  • Learn advanced Design Patterns in Software Engineering
  • Master Python OOP Concepts for Beginners to Advanced
  • Explore Clean Code Best Practices for Developers
  • Understand Software Architecture Fundamentals

By practicing these concepts regularly, you’ll move closer to becoming a professional software engineer ready for the Pakistani and global tech industry 🚀

Practice the code examples from this tutorial
Open Compiler
Share this tutorial:

Test Your Python Knowledge!

Finished reading? Take a quick quiz to see how much you've learned from this tutorial.

Start Python Quiz

About Zaheer Ahmad