Popular Searches
Popular Course Categories
Popular Courses

Designing Reusable and Maintainable Object-Oriented Applications

Designing Reusable and Maintainable Object-Oriented Applications

Advanced Object-Oriented Python Concepts

Designing Reusable and Maintainable Object-Oriented Applications in Python

Course: Python Training

Topic: Designing Reusable and Maintainable Object-Oriented Applications

Course Link: Python Training Course

Demo Link: Register for a Course Demo

Website: JustAcademy


1. Introduction to Reusable and Maintainable Applications

Object-Oriented Programming (OOP) is a programming approach that organizes software around classes and objects. It helps developers combine data and behavior into logical units, making applications easier to understand, extend, test, and maintain.

Designing reusable and maintainable applications means creating software components that can be used in multiple situations, modified without unnecessary side effects, and understood by other developers. A well-designed application should be easy to improve when business requirements change.

For example, an e-commerce application may contain separate classes for customers, products, shopping carts, orders, payments, and notifications. Each class has a clear responsibility and collaborates with other classes to complete the application's tasks.

Learning Objectives

  • Understand the principles of reusable and maintainable software design.
  • Learn how to design classes with clear responsibilities.
  • Apply encapsulation, abstraction, inheritance, and polymorphism appropriately.
  • Understand composition, aggregation, and dependency injection.
  • Reduce duplicate code by using reusable methods and components.
  • Learn SOLID principles and their application in Python.
  • Improve code readability, testing, documentation, and error handling.
  • Build a small maintainable application using object-oriented design.

2. What Is Code Reusability?

Code reusability is the practice of writing code that can be used in multiple parts of an application or across different projects without repeatedly implementing the same functionality.

Reusable code reduces duplication, improves consistency, and makes updates easier. When a reusable component needs to change, developers can update it in one place instead of modifying several copies of the same logic.

Example Without Code Reusability

customer1_name = "Amit"
customer1_price = 1500
customer1_tax = customer1_price * 0.18
customer1_total = customer1_price + customer1_tax

customer2_name = "Priya"
customer2_price = 2500
customer2_tax = customer2_price * 0.18
customer2_total = customer2_price + customer2_tax

print(customer1_name, customer1_total)
print(customer2_name, customer2_total)

This example repeats the tax calculation for every customer. If the tax calculation changes, each repeated section may need to be updated.

Improved Example Using a Reusable Function

def calculate_total(price, tax_rate=0.18):
    tax = price * tax_rate
    return price + tax


print(calculate_total(1500))
print(calculate_total(2500))

Output

1770.0
2950.0

The calculation is defined once and reused for multiple prices. The same principle can be applied to class methods, services, modules, and larger application components.

Benefits of Code Reusability

  • Reduces repeated code.
  • Improves consistency across the application.
  • Makes changes easier to implement.
  • Reduces the chance of fixing a bug in one place but missing another copy.
  • Makes testing and debugging more efficient.
  • Encourages modular software architecture.

3. What Is Maintainability?

Maintainability is the ease with which developers can understand, correct, test, modify, and extend software after it has been created.

A maintainable application is not necessarily the application with the fewest lines of code. It is an application whose structure makes its behavior understandable and whose components can be changed without creating unnecessary problems elsewhere.

Characteristics of Maintainable Software

  • Readability: Code is easy to understand.
  • Modularity: The application is divided into logical components.
  • Low coupling: Components depend as little as practical on one another's internal details.
  • High cohesion: Each class focuses on closely related responsibilities.
  • Testability: Components can be tested independently.
  • Extensibility: New features can be added without extensive rewrites.
  • Documentation: Important decisions and public interfaces are explained.
  • Consistency: Naming, formatting, and project structure follow recognizable conventions.

Example of Difficult-to-Maintain Code

class Application:
    def run(self):
        # Read customer data
        # Validate customer data
        # Calculate order amount
        # Process payment
        # Save order
        # Send email notification
        print("Everything completed")

This class combines several unrelated responsibilities. If the payment provider changes or the notification method needs updating, the same class may require modifications in several places.

Improved Design

class CustomerValidator:
    def validate(self, customer):
        return bool(customer.get("name"))


class PriceCalculator:
    def calculate(self, price, quantity):
        return price * quantity


class EmailNotifier:
    def send(self, message):
        print(f"Email notification: {message}")


validator = CustomerValidator()
calculator = PriceCalculator()
notifier = EmailNotifier()

customer = {"name": "Amit"}

if validator.validate(customer):
    total = calculator.calculate(500, 2)
    notifier.send(f"Order total is ₹{total}")

Output

Email notification: Order total is ₹1000

Each class now has a focused responsibility. This makes the individual operations easier to understand, test, replace, and extend.


4. Core Principles of Object-Oriented Programming

Four foundational OOP principles help developers design structured and reusable applications.

4.1 Encapsulation

Encapsulation combines data and related methods in a class and provides a controlled way to interact with the object's state.

class BankAccount:
    def __init__(self, balance=0):
        if balance < 0:
            raise ValueError("Balance cannot be negative")
        self._balance = balance

    def deposit(self, amount):
        if amount <= 0:
            raise ValueError("Deposit must be positive")
        self._balance += amount

    def get_balance(self):
        return self._balance


account = BankAccount(1000)
account.deposit(500)
print(account.get_balance())

Output

1500

The balance is updated through a method that validates the operation. The underscore in _balance signals that it is intended for internal use; Python does not enforce strict private access for a single-underscore attribute.

4.2 Abstraction

Abstraction exposes essential operations while hiding implementation details. Abstract base classes can define a common contract for related implementations.

from abc import ABC, abstractmethod

class ReportExporter(ABC):
    @abstractmethod
    def export(self, data):
        pass


class TextExporter(ReportExporter):
    def export(self, data):
        return "\n".join(map(str, data))


exporter = TextExporter()
print(exporter.export(["Sales", 100, 200]))

Output

Sales
100
200

4.3 Inheritance

Inheritance allows a class to reuse or specialize behavior from another class. It should be used when the subclass represents a meaningful specialized form of the parent class.

class Employee:
    def __init__(self, name):
        self.name = name

    def display(self):
        return self.name


class Developer(Employee):
    def work(self):
        return f"{self.name} writes code"


developer = Developer("Amit")
print(developer.display())
print(developer.work())

Output

Amit
Amit writes code

4.4 Polymorphism

Polymorphism allows different objects to respond to the same operation according to their own implementations.

class PDFReport:
    def export(self):
        return "Exporting PDF report"


class CSVReport:
    def export(self):
        return "Exporting CSV report"


def generate_report(report):
    print(report.export())


generate_report(PDFReport())
generate_report(CSVReport())

Output

Exporting PDF report
Exporting CSV report

The function works with either object because both provide the required export() method. This is an example of polymorphism through duck typing.


5. Designing Classes with Clear Responsibilities

A class should represent a meaningful concept and contain data and behavior that belong together. A class that handles too many unrelated tasks becomes difficult to understand, test, and change.

Example of a Poorly Designed Class

class EmployeeManager:
    def add_employee(self, employee):
        print("Employee added")

    def calculate_salary(self, salary, bonus):
        return salary + bonus

    def generate_report(self):
        print("Report generated")

    def send_email(self, email):
        print(f"Email sent to {email}")

This class manages employees, calculates salaries, generates reports, and sends emails. These responsibilities may change for different reasons, which makes the class harder to maintain.

Improved Class Design

class EmployeeRepository:
    def add_employee(self, employee):
        print(f"Employee saved: {employee}")


class SalaryCalculator:
    def calculate(self, salary, bonus):
        return salary + bonus


class ReportGenerator:
    def generate(self, employees):
        return f"Report contains {len(employees)} employees"


class EmailService:
    def send(self, email):
        print(f"Email sent to {email}")

Advantages

  • Each class has a clear purpose.
  • Individual classes can be tested separately.
  • Changing the email implementation does not require rewriting salary calculations.
  • New implementations can be introduced without changing unrelated components.
  • Other developers can locate the relevant functionality more easily.

6. Understanding High Cohesion and Low Coupling

High cohesion and low coupling are important design goals for reusable and maintainable applications.

High Cohesion

Cohesion describes how closely the responsibilities within a module or class relate to one another. A highly cohesive class focuses on one clear area of functionality.

For example, a TaxCalculator should calculate taxes rather than also managing user accounts and sending emails.

Low Coupling

Coupling describes the degree of dependency between components. Low coupling means one component does not rely unnecessarily on the internal details of another component.

Example of Tight Coupling

class EmailService:
    def send(self, message):
        print(f"Email sent: {message}")


class OrderService:
    def __init__(self):
        self.email_service = EmailService()

    def complete_order(self):
        print("Order completed")
        self.email_service.send("Your order is complete")

The OrderService directly creates an EmailService. Replacing email notifications with SMS notifications may require changes to the order service.

Example with Dependency Injection

class EmailService:
    def send(self, message):
        print(f"Email sent: {message}")


class SMSService:
    def send(self, message):
        print(f"SMS sent: {message}")


class OrderService:
    def __init__(self, notifier):
        self.notifier = notifier

    def complete_order(self):
        print("Order completed")
        self.notifier.send("Your order is complete")


order_service = OrderService(EmailService())
order_service.complete_order()

sms_order_service = OrderService(SMSService())
sms_order_service.complete_order()

Output

Order completed
Email sent: Your order is complete
Order completed
SMS sent: Your order is complete

The notification dependency is supplied from outside the class. This is called dependency injection. It makes the order service more flexible because different notification implementations can be supplied without changing its core logic.


7. Applying SOLID Principles

SOLID is a group of five object-oriented design principles that help developers build flexible, understandable, and maintainable software.

Principle Meaning Purpose
Single Responsibility Principle (SRP) A class should have one primary responsibility. Limits unrelated reasons for changing a class.
Open/Closed Principle (OCP) Software entities should be open for extension but closed for unnecessary modification. Supports adding new behavior through suitable abstractions and implementations.
Liskov Substitution Principle (LSP) Subtypes should remain usable wherever their base type is expected. Prevents subclasses from breaking established behavior.
Interface Segregation Principle (ISP) Clients should not depend on methods they do not need. Encourages smaller, focused interfaces.
Dependency Inversion Principle (DIP) High-level policy should depend on abstractions rather than concrete implementation details. Reduces tight coupling between application logic and external services.

7.1 Single Responsibility Principle

A class should have one primary responsibility and a clear reason to change.

class Invoice:
    def __init__(self, amount):
        self.amount = amount


class InvoicePrinter:
    def print_invoice(self, invoice):
        print(f"Invoice amount: ₹{invoice.amount}")


invoice = Invoice(2500)
printer = InvoicePrinter()
printer.print_invoice(invoice)

7.2 Open/Closed Principle

New functionality should be introduced through extension points when possible instead of repeatedly modifying stable code.

from abc import ABC, abstractmethod


class Discount(ABC):
    @abstractmethod
    def apply(self, amount):
        pass


class NoDiscount(Discount):
    def apply(self, amount):
        return amount


class PercentageDiscount(Discount):
    def __init__(self, percentage):
        self.percentage = percentage

    def apply(self, amount):
        return amount * (1 - self.percentage / 100)


def calculate_price(amount, discount):
    return discount.apply(amount)


print(calculate_price(1000, NoDiscount()))
print(calculate_price(1000, PercentageDiscount(10)))

Output

1000
900.0

A new discount strategy can be added by implementing the Discount abstraction without rewriting the calculate_price() function.

7.3 Liskov Substitution Principle

A subclass should preserve the behavior promised by its base type. A class should not inherit from another type if its behavior fundamentally violates the expectations of that type.

class PaymentMethod:
    def pay(self, amount):
        raise NotImplementedError


class CardPayment(PaymentMethod):
    def pay(self, amount):
        return f"Card payment processed: ₹{amount}"


class UpiPayment(PaymentMethod):
    def pay(self, amount):
        return f"UPI payment processed: ₹{amount}"


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


print(checkout(CardPayment(), 500))
print(checkout(UpiPayment(), 800))

Both implementations support the expected payment operation, so the checkout function can use either one. A production implementation should also define consistent failure and transaction behavior.

7.4 Interface Segregation Principle

Interfaces should remain focused so that a class is not forced to implement unrelated operations.

from abc import ABC, abstractmethod


class Printable(ABC):
    @abstractmethod
    def print_document(self):
        pass


class Scannable(ABC):
    @abstractmethod
    def scan_document(self):
        pass


class BasicPrinter(Printable):
    def print_document(self):
        return "Document printed"


class MultiFunctionPrinter(Printable, Scannable):
    def print_document(self):
        return "Document printed"

    def scan_document(self):
        return "Document scanned"


print(BasicPrinter().print_document())
print(MultiFunctionPrinter().scan_document())

A basic printer only implements printing, while a multifunction printer implements both printing and scanning. The design avoids forcing every printer to support scanning.

7.5 Dependency Inversion Principle

High-level application logic should rely on an abstraction rather than directly depending on a particular service implementation.

from abc import ABC, abstractmethod


class MessageSender(ABC):
    @abstractmethod
    def send(self, message):
        pass


class EmailSender(MessageSender):
    def send(self, message):
        print(f"Email: {message}")


class AlertManager:
    def __init__(self, sender):
        self.sender = sender

    def notify(self, message):
        self.sender.send(message)


manager = AlertManager(EmailSender())
manager.notify("System backup completed")

The alert manager receives a message sender instead of constructing a specific sender internally. Another compatible sender can be introduced with minimal changes to the alert manager.


8. Composition Over Inheritance

Composition allows one object to use other objects to perform its work. It is often preferable to deep inheritance hierarchies when an application needs flexible combinations of behavior.

Inheritance Example

class Vehicle:
    def start(self):
        return "Vehicle started"


class Car(Vehicle):
    pass


car = Car()
print(car.start())

Composition Example

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


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

    def start(self):
        return self.engine.start()


car = Car(Engine())
print(car.start())

Output

Engine started

Composition makes it possible to supply different engine implementations without changing the car's basic interface. Inheritance remains appropriate when a genuine is-a relationship exists, but composition is often more flexible for combining independent behaviors.

When to Use Composition

  • When a class needs services supplied by other objects.
  • When dependencies may need to be replaced.
  • When behavior should be combined without creating a large class hierarchy.
  • When components need to be tested independently.

9. Designing Reusable Methods and Classes

Reusable methods perform a clearly defined operation and accept parameters instead of relying on hard-coded values. Reusable classes encapsulate related state and behavior so they can be used in different parts of an application.

Example: Reusable Product Class

class Product:
    def __init__(self, name, price):
        if price < 0:
            raise ValueError("Price cannot be negative")

        self.name = name
        self.price = price

    def calculate_total(self, quantity):
        if quantity <= 0:
            raise ValueError("Quantity must be positive")

        return self.price * quantity


keyboard = Product("Keyboard", 1200)
mouse = Product("Mouse", 600)

print(keyboard.calculate_total(2))
print(mouse.calculate_total(3))

Output

2400
1800

The same class works with different product names, prices, and quantities. Validation is centralized in the class, reducing the need to repeat these checks throughout the application.

Guidelines for Reusable Methods

  • Use meaningful method names that describe the action.
  • Accept variable information through parameters.
  • Return results when callers need to use them.
  • Avoid unnecessary global variables.
  • Validate input where the method's responsibility requires it.
  • Keep methods focused on one operation.
  • Document important assumptions and side effects.

10. Using Modules and Packages

Modules and packages organize reusable code into separate files and directories. This makes large applications easier to navigate and maintain.

What Is a Module?

A module is generally a Python file containing classes, functions, constants, or related logic that can be imported by another file.

Example Project Structure

shop_app/
    main.py
    models.py
    services.py
    validators.py
    notifications.py

models.py

class Product:
    def __init__(self, name, price):
        self.name = name
        self.price = price

services.py

class PriceCalculator:
    def calculate_total(self, product, quantity):
        return product.price * quantity

main.py

from models import Product
from services import PriceCalculator

product = Product("Keyboard", 1200)
calculator = PriceCalculator()

total = calculator.calculate_total(product, 2)
print(f"Total: ₹{total}")

Output

Total: ₹2400

The product model and price calculation service are kept in separate modules. This separation allows each component to evolve independently while remaining available to other parts of the application.

Benefits of Modules and Packages

  • Organize code by responsibility.
  • Make components easier to locate and test.
  • Encourage reuse across multiple files.
  • Reduce the complexity of individual source files.
  • Make larger applications easier to collaborate on.

11. Naming Conventions and Readability

Readable code is easier to maintain because developers spend less time understanding what a class or method does. Python code commonly follows conventions described in PEP 8.

Element Recommended Style Example
Class names CapWords or PascalCase OrderService
Function names Lowercase with underscores calculate_total()
Method names Lowercase with underscores validate_order()
Constants Uppercase with underscores DEFAULT_TAX_RATE
Internal attributes Leading underscore by convention _balance
Module names Short lowercase names order_service.py

Example of Poor Naming

class X:
    def f(self, a, b):
        return a * b

Improved Naming

class PriceCalculator:
    def calculate_total(self, price, quantity):
        return price * quantity

The second example clearly communicates the class's purpose and the method's operation. Good names reduce the need for excessive comments.


12. Documentation and Docstrings

Documentation explains how a class, method, or module should be used. Python docstrings are string literals placed at the beginning of a module, class, or function to describe its purpose.

Example

class TemperatureConverter:
    """Convert temperatures between Celsius and Fahrenheit."""

    def celsius_to_fahrenheit(self, celsius):
        """Convert Celsius to Fahrenheit.

        Args:
            celsius: Temperature in degrees Celsius.

        Returns:
            Temperature in degrees Fahrenheit.
        """
        return (celsius * 9 / 5) + 32


converter = TemperatureConverter()
print(converter.celsius_to_fahrenheit(25))
print(TemperatureConverter.__doc__)

Output

77.0
Convert temperatures between Celsius and Fahrenheit.

Documentation Best Practices

  • Document the purpose of public classes and methods.
  • Explain parameters and return values when they are not obvious.
  • Describe important exceptions and side effects.
  • Keep documentation consistent with actual behavior.
  • Update documentation when the public interface changes.
  • Avoid comments that merely repeat what the code already makes obvious.

13. Error Handling and Exception Design

Reliable applications handle expected errors clearly instead of allowing failures to produce confusing behavior. Exceptions help communicate when an operation cannot be completed.

Example of Input Validation

class BankAccount:
    def __init__(self, balance=0):
        if balance < 0:
            raise ValueError("Initial balance cannot be negative")
        self.balance = balance

    def withdraw(self, amount):
        if amount <= 0:
            raise ValueError("Withdrawal must be positive")

        if amount > self.balance:
            raise ValueError("Insufficient balance")

        self.balance -= amount
        return self.balance


account = BankAccount(2000)

try:
    print(account.withdraw(500))
    print(account.withdraw(3000))
except ValueError as error:
    print(f"Transaction failed: {error}")

Output

1500
Transaction failed: Insufficient balance

Best Practices for Error Handling

  • Raise meaningful exceptions when an operation violates a requirement.
  • Use built-in exception types such as ValueError and TypeError when appropriate.
  • Create custom exception classes for meaningful application-specific errors.
  • Catch exceptions at a level where the program can handle them appropriately.
  • Avoid catching every exception without a clear reason.
  • Do not silently ignore errors that affect correctness.
  • Keep error messages helpful without exposing passwords, tokens, or other sensitive information.

14. Unit Testing Object-Oriented Applications

Unit testing verifies that individual functions, methods, and classes behave as expected. Testing is especially important in reusable applications because a change to a shared component can affect multiple parts of the system.

Example Class to Test

class Calculator:
    def add(self, a, b):
        return a + b

    def multiply(self, a, b):
        return a * b

Unit Test Using unittest

import unittest


class Calculator:
    def add(self, a, b):
        return a + b

    def multiply(self, a, b):
        return a * b


class TestCalculator(unittest.TestCase):
    def setUp(self):
        self.calculator = Calculator()

    def test_add(self):
        self.assertEqual(self.calculator.add(2, 3), 5)

    def test_multiply(self):
        self.assertEqual(self.calculator.multiply(4, 3), 12)


if __name__ == "__main__":
    unittest.main()

Explanation

  • unittest.TestCase provides the testing framework.
  • setUp() prepares a calculator instance for each test.
  • assertEqual() checks whether the actual and expected results match.
  • Each test focuses on one behavior.

Save the test in a Python file and run it with the Python interpreter. Automated tests help identify regressions when reusable components are changed.

Testing Best Practices

  • Test normal inputs and boundary cases.
  • Test invalid inputs and expected exceptions.
  • Test classes independently where practical.
  • Use mocks or test doubles for external services when appropriate.
  • Keep tests readable and deterministic.
  • Run tests after significant changes and before releases.

15. Refactoring for Better Maintainability

Refactoring means improving the internal structure of existing code without intentionally changing its externally observable behavior.

Developers refactor code when they find duplication, overly large classes, unclear names, long methods, or unnecessary dependencies.

Before Refactoring

def calculate_book_total(price, quantity):
    return price * quantity


def calculate_laptop_total(price, quantity):
    return price * quantity


def calculate_phone_total(price, quantity):
    return price * quantity

All three functions perform the same calculation. Maintaining separate copies adds unnecessary duplication.

After Refactoring

def calculate_total(price, quantity):
    return price * quantity


print(calculate_total(500, 3))
print(calculate_total(40000, 1))
print(calculate_total(15000, 2))

Output

1500
40000
30000

Common Refactoring Techniques

  • Extract repeated logic into a reusable function or method.
  • Split large classes into smaller, focused classes.
  • Rename variables and methods to improve clarity.
  • Replace long conditional logic with appropriate strategies or polymorphism.
  • Introduce abstractions when they solve a genuine design problem.
  • Remove dead code and unnecessary dependencies.
  • Run automated tests to verify that behavior remains correct.

16. Avoiding Common Object-Oriented Design Problems

Problem Description Recommended Improvement
God object One class handles too many unrelated responsibilities. Split responsibilities into cohesive classes.
Code duplication The same business logic appears in multiple places. Extract reusable functions, methods, or components.
Tight coupling A class directly depends on many concrete implementations. Use focused interfaces and dependency injection where appropriate.
Deep inheritance hierarchy Behavior becomes difficult to trace across many parent classes. Prefer simpler hierarchies or composition where appropriate.
Large methods A method performs several unrelated operations. Break it into smaller methods with clear purposes.
Unclear naming Names do not communicate responsibilities. Use descriptive, consistent naming conventions.
Excessive abstraction Unnecessary classes and interfaces increase complexity. Introduce abstractions only when they provide practical value.
Uncontrolled shared state Many components modify the same mutable data. Clarify ownership and expose controlled operations.
Missing tests Changes can break existing behavior without detection. Add automated unit and integration tests.

17. Designing an Application Architecture

Application architecture describes how major parts of a system are organized and interact. A simple layered architecture can separate user interaction, business logic, and data storage.

Common Application Layers

  • Presentation layer: Handles user input and displays results.
  • Service layer: Coordinates business operations and application workflows.
  • Domain or model layer: Represents business entities and their rules.
  • Repository or data-access layer: Handles data persistence and retrieval.
  • Infrastructure layer: Provides external integrations such as email, payment gateways, or databases.

Example Architecture

Presentation
     |
     v
OrderService
     |
     +------> Product / Order Models
     |
     +------> OrderRepository
     |
     +------> NotificationService

The service coordinates the operation without needing to implement every detail of data storage or notification delivery itself.

Example Implementation

class Order:
    def __init__(self, order_id, amount):
        if amount < 0:
            raise ValueError("Amount cannot be negative")

        self.order_id = order_id
        self.amount = amount


class OrderRepository:
    def save(self, order):
        print(f"Order {order.order_id} saved")


class NotificationService:
    def send(self, message):
        print(f"Notification: {message}")


class OrderService:
    def __init__(self, repository, notifier):
        self.repository = repository
        self.notifier = notifier

    def place_order(self, order):
        self.repository.save(order)
        self.notifier.send(
            f"Order {order.order_id} placed successfully"
        )


repository = OrderRepository()
notifier = NotificationService()
service = OrderService(repository, notifier)

order = Order("ORD101", 2500)
service.place_order(order)

Output

Order ORD101 saved
Notification: Order ORD101 placed successfully

This design separates the order model, persistence operation, notification operation, and application workflow. A real application could replace the repository with a database implementation and the notifier with an email or messaging provider.


18. Version Control and Team Collaboration

Reusable and maintainable applications are easier to develop when teams follow consistent collaboration practices. Version control systems such as Git help developers track changes, work on separate features, and review modifications before merging them.

Recommended Practices

  • Use descriptive commit messages.
  • Keep changes focused on one feature or fix when possible.
  • Use branches for independent development tasks.
  • Review code before merging important changes.
  • Run automated tests during the development workflow.
  • Document significant architectural decisions.
  • Avoid committing passwords, API keys, or other secrets.
  • Use dependency files to make project environments easier to reproduce.

Example Git Workflow

git status
git checkout -b feature/order-service
git add .
git commit -m "Add reusable order service"
git push -u origin feature/order-service

These commands illustrate a common branch-based workflow. The exact process depends on the team's repository policies and collaboration platform.


19. Performance and Maintainability

Maintainable code should also perform adequately for its intended workload. However, optimizing code prematurely can make an application more complicated without providing meaningful benefits.

Recommended Practices

  • Choose suitable data structures and algorithms.
  • Avoid repeating expensive calculations unnecessarily.
  • Use generators when processing large streams of data where appropriate.
  • Measure performance before making complex optimizations.
  • Cache results only when the benefit and invalidation rules are understood.
  • Keep performance-sensitive code covered by tests.
  • Use profiling tools to identify actual bottlenecks.
  • Balance optimization with readability and simplicity.

Example: Avoiding Repeated Calculation

class Report:
    def __init__(self, values):
        self.values = values

    def calculate_total(self):
        return sum(self.values)

    def calculate_average(self):
        if not self.values:
            return 0
        return self.calculate_total() / len(self.values)


report = Report([10, 20, 30, 40])

print(report.calculate_total())
print(report.calculate_average())

Output

100
25.0

The total calculation is reused by the average calculation, avoiding duplicate implementation of the summation logic. Whether caching is necessary depends on the size of the data and how frequently the values change.


20. Practical Exercises

Exercise 1: Student Management System

Create a Student class with student details and a StudentService class to validate and register students. Keep data representation separate from registration logic.

Exercise 2: Employee Payroll System

Create separate classes for employees, salary calculations, and payroll reporting. Implement reusable salary calculations and validate salary inputs.

Exercise 3: Notification System

Create a notification abstraction with email and SMS implementations. Inject the chosen notification service into a separate application service.

Exercise 4: Product Inventory System

Create a product model, inventory service, and inventory repository. Add methods to register products, update quantities, and retrieve stock information.

Exercise 5: Report Generator

Implement CSV and text exporters that follow a common interface. Make a report service that can use either exporter without rewriting its main workflow.

Exercise 6: Refactoring Practice

Take a class that performs data validation, business calculations, database operations, and notification delivery. Refactor it into smaller classes with clear responsibilities and test each component.


21. Mini Project: Maintainable E-Commerce Order Management System

Project Goal: Build a small object-oriented order management system using reusable classes, dependency injection, validation, and separation of responsibilities.

Project Requirements

  • Create a Product class to represent products and their prices.
  • Create an OrderItem class to represent a product and its quantity.
  • Create an Order class to calculate the total amount.
  • Create a repository class to save orders.
  • Create a notification service to inform the customer.
  • Create an order service to coordinate the workflow.
  • Validate product prices and order quantities.
  • Keep the design flexible enough to replace the repository or notifier.

Sample Implementation

class Product:
    def __init__(self, name, price):
        if price < 0:
            raise ValueError("Price cannot be negative")

        self.name = name
        self.price = price


class OrderItem:
    def __init__(self, product, quantity):
        if quantity <= 0:
            raise ValueError("Quantity must be positive")

        self.product = product
        self.quantity = quantity

    def subtotal(self):
        return self.product.price * self.quantity


class Order:
    def __init__(self, order_id, items):
        if not items:
            raise ValueError("Order must contain at least one item")

        self.order_id = order_id
        self.items = list(items)

    def total(self):
        return sum(item.subtotal() for item in self.items)


class OrderRepository:
    def save(self, order):
        print(f"Saved order: {order.order_id}")


class NotificationService:
    def send(self, message):
        print(f"Notification: {message}")


class OrderService:
    def __init__(self, repository, notifier):
        self.repository = repository
        self.notifier = notifier

    def place_order(self, order):
        total = order.total()
        self.repository.save(order)
        self.notifier.send(
            f"Order {order.order_id} placed. Total: ₹{total}"
        )
        return total


product1 = Product("Keyboard", 1200)
product2 = Product("Mouse", 600)

items = [
    OrderItem(product1, 2),
    OrderItem(product2, 1)
]

order = Order("ORD101", items)

service = OrderService(
    OrderRepository(),
    NotificationService()
)

print("Order total:", service.place_order(order))

Output

Saved order: ORD101
Notification: Order ORD101 placed. Total: ₹3000
Order total: 3000

Project Design Explanation

  • Product: Stores product information and validates the price.
  • OrderItem: Represents a product quantity and calculates the subtotal.
  • Order: Groups order items and calculates the total.
  • OrderRepository: Handles the saving responsibility.
  • NotificationService: Handles notification delivery.
  • OrderService: Coordinates the order workflow using injected dependencies.

The repository and notification implementations are demonstrations. A production system would need persistent database storage, reliable notification delivery, transaction management, and additional validation.

Possible Enhancements

  • Add customer details and shipping addresses.
  • Introduce a payment service abstraction.
  • Implement database persistence.
  • Add discounts and tax calculations.
  • Create unit tests for totals, validation, and order placement.
  • Use a logging module to record failures and important events.
  • Add order statuses such as pending, confirmed, and cancelled.

22. Interview Questions and Answers

Q1. What is code reusability?

Code reusability is the practice of designing functions, classes, and modules so they can be used in multiple situations without duplicating their implementation.

Q2. What is maintainability?

Maintainability describes how easily software can be understood, corrected, tested, modified, and extended.

Q3. What are the four main principles of OOP?

The four commonly recognized principles are encapsulation, abstraction, inheritance, and polymorphism.

Q4. What is the Single Responsibility Principle?

It states that a class should have one primary responsibility and a focused reason to change.

Q5. What is the Open/Closed Principle?

It encourages designing software so new behavior can be added through extension without repeatedly modifying stable, existing code.

Q6. What is dependency injection?

Dependency injection means supplying an object with the services or dependencies it needs instead of requiring it to construct every dependency itself.

Q7. What is the difference between high cohesion and low coupling?

High cohesion means the responsibilities inside a component are closely related. Low coupling means components have fewer unnecessary dependencies on one another.

Q8. Why is composition often preferred over inheritance?

Composition can make it easier to combine and replace behaviors without creating a rigid class hierarchy. Inheritance is still useful when a genuine is-a relationship exists.

Q9. What is refactoring?

Refactoring improves a program's internal structure without intentionally changing its externally observable behavior.

Q10. How do modules improve maintainability?

Modules group related functionality into separate files, making code easier to locate, reuse, test, and maintain.

Q11. Why are unit tests important?

Unit tests verify the behavior of individual components and help detect regressions when code changes.

Q12. What is the Liskov Substitution Principle?

It requires subtypes to preserve the behavioral expectations of their base types so that they can be substituted without breaking the program.

Q13. What is the Interface Segregation Principle?

It recommends small, focused interfaces so that classes do not depend on operations they do not need.

Q14. How can duplicate code be reduced?

Repeated logic can be extracted into reusable functions, methods, classes, or suitable shared components.

Q15. What makes a class difficult to maintain?

A class becomes difficult to maintain when it has too many responsibilities, unclear names, duplicated logic, excessive dependencies, or insufficient tests.

Q16. Why is documentation important?

Documentation explains how components should be used, clarifies important assumptions, and helps other developers understand the application.

Q17. How do you make a class easier to test?

Give it a clear responsibility, minimize unnecessary dependencies, inject external services where appropriate, and make its behavior predictable.

Q18. What is the Dependency Inversion Principle?

It encourages high-level business logic to depend on abstractions rather than concrete implementation details.

Q19. What is the difference between reusable and maintainable code?

Reusable code can be applied in multiple contexts. Maintainable code is easy to understand, modify, debug, and extend. Good software often aims to achieve both.

Q20. How do you decide whether to introduce an abstraction?

Introduce an abstraction when it clarifies responsibilities, supports meaningful variation, reduces coupling, or improves testing. Avoid abstractions that add complexity without practical benefits.


23. Quick Revision Notes

  • Reusable code avoids unnecessary duplication.
  • Maintainable code is readable, testable, modular, and easy to change.
  • Encapsulation organizes data and behavior and provides controlled access to object state.
  • Abstraction exposes essential operations while hiding implementation details.
  • Inheritance supports specialization and reuse through a parent-child relationship.
  • Polymorphism allows different objects to respond to a common operation.
  • High cohesion and low coupling improve software design.
  • SOLID principles help structure flexible object-oriented applications.
  • Composition and dependency injection help reduce unnecessary coupling.
  • Modules and packages organize reusable code.
  • Meaningful naming and docstrings improve readability.
  • Exception handling makes failures easier to understand and manage.
  • Unit tests protect existing behavior during refactoring.
  • Version control and code reviews support reliable team development.
  • Abstractions should solve real problems rather than add unnecessary complexity.

Object-Oriented Design Checklist

  • Does every class have a clear responsibility?
  • Is duplicated business logic minimized?
  • Are class and method names descriptive?
  • Are dependencies easy to replace when necessary?
  • Are inputs validated where appropriate?
  • Are errors handled clearly?
  • Are public methods and important assumptions documented?
  • Are critical behaviors covered by automated tests?
  • Can new features be added without unnecessary changes to unrelated components?
  • Is the design simpler than the problem requires, rather than more complicated?

24. Conclusion

Designing reusable and maintainable object-oriented applications requires more than creating classes and objects. Developers must carefully organize responsibilities, reduce duplication, manage dependencies, and create clear interfaces between components.

Principles such as encapsulation, abstraction, polymorphism, composition, high cohesion, low coupling, and SOLID provide practical guidance for building software that can grow with changing requirements. Meaningful naming, documentation, exception handling, unit testing, and refactoring further improve the quality of an application.

The best design is not necessarily the most complex one. It is the design that solves the problem clearly, supports necessary changes, and remains understandable to the people who maintain it.

Continue learning object-oriented programming and Python development through the Python Training Course at JustAcademy. Visit the JustAcademy website or register for a course demo.

whatsapp