Skip to content

Object-Oriented Design Principles

Object-oriented design is a fundamental approach to organizing and structuring code. While it's often associated with high-level languages like Java and Python, understanding OOP principles is crucial for systems programmers who need to design maintainable, extensible, and robust systems.

Let's explore the core principles of object-oriented design and understand how they apply to systems programming.

The Four Pillars of Object-Oriented Programming

Encapsulation: Hiding Implementation Details

Encapsulation is the bundling of data and the methods that operate on that data, while hiding the internal state and implementation details.

Why it matters:

cpp
// Bad: Exposed implementation details
class BankAccount {
public:
    double balance;  // Anyone can modify this directly
    void deposit(double amount) {
        balance += amount;  // No validation
    }
};

// Good: Encapsulated implementation
class BankAccount {
private:
    double balance;
    std::string accountNumber;

public:
    BankAccount(const std::string& number) : accountNumber(number), balance(0.0) {}

    void deposit(double amount) {
        if (amount > 0) {
            balance += amount;
        }
    }

    double getBalance() const {
        return balance;
    }
};

Benefits:

  • Data integrity: Prevents invalid state changes
  • Implementation flexibility: Can change internal implementation without affecting clients
  • Reduced coupling: Clients don't depend on internal details

Inheritance: Establishing "Is-A" Relationships

Inheritance allows a class to inherit properties and methods from another class, establishing an "is-a" relationship.

Example:

cpp
class Animal {
protected:
    std::string name;

public:
    Animal(const std::string& n) : name(n) {}
    virtual void makeSound() = 0;  // Pure virtual function
    virtual ~Animal() = default;
};

class Dog : public Animal {
public:
    Dog(const std::string& n) : Animal(n) {}

    void makeSound() override {
        std::cout << name << " says: Woof!" << std::endl;
    }
};

class Cat : public Animal {
public:
    Cat(const std::string& n) : Animal(n) {}

    void makeSound() override {
        std::cout << name << " says: Meow!" << std::endl;
    }
};

When to use inheritance:

  • True "is-a" relationships: A Dog is an Animal
  • Code reuse: Common functionality in base class
  • Polymorphism: Treating derived classes uniformly

Polymorphism: Treating Objects Uniformly

Polymorphism allows you to treat objects of different types uniformly through a common interface.

Example:

cpp
void animalConcert(const std::vector<std::unique_ptr<Animal>>& animals) {
    for (const auto& animal : animals) {
        animal->makeSound();  // Calls the appropriate derived class method
    }
}

// Usage
std::vector<std::unique_ptr<Animal>> animals;
animals.push_back(std::make_unique<Dog>("Buddy"));
animals.push_back(std::make_unique<Cat>("Whiskers"));
animalConcert(animals);

Types of polymorphism:

  • Compile-time (static): Function overloading, templates
  • Runtime (dynamic): Virtual functions, inheritance

Abstraction: Simplifying Complex Systems

Abstraction focuses on what an object does rather than how it does it.

Example:

cpp
// High-level abstraction
class DatabaseConnection {
public:
    virtual void connect() = 0;
    virtual void disconnect() = 0;
    virtual void executeQuery(const std::string& query) = 0;
    virtual ~DatabaseConnection() = default;
};

// Concrete implementation
class MySQLConnection : public DatabaseConnection {
private:
    MYSQL* connection;

public:
    void connect() override {
        // Complex MySQL connection logic
    }

    void disconnect() override {
        // MySQL disconnection logic
    }

    void executeQuery(const std::string& query) override {
        // MySQL query execution
    }
};

Single Responsibility Principle (SRP)

A class should have only one reason to change.

Bad example:

cpp
class OrderProcessor {
public:
    void processOrder(Order& order) {
        validateOrder(order);
        saveToDatabase(order);
        sendEmailNotification(order);
        updateInventory(order);
    }

private:
    void validateOrder(Order& order) { /* ... */ }
    void saveToDatabase(Order& order) { /* ... */ }
    void sendEmailNotification(Order& order) { /* ... */ }
    void updateInventory(Order& order) { /* ... */ }
};

Good example:

cpp
class OrderValidator {
public:
    bool validate(Order& order) { /* ... */ }
};

class OrderRepository {
public:
    void save(Order& order) { /* ... */ }
};

class NotificationService {
public:
    void sendEmail(const Order& order) { /* ... */ }
};

class InventoryManager {
public:
    void updateStock(const Order& order) { /* ... */ }
};

class OrderProcessor {
private:
    OrderValidator validator;
    OrderRepository repository;
    NotificationService notifier;
    InventoryManager inventory;

public:
    void processOrder(Order& order) {
        if (validator.validate(order)) {
            repository.save(order);
            notifier.sendEmail(order);
            inventory.updateStock(order);
        }
    }
};

Open/Closed Principle (OCP)

Software entities should be open for extension but closed for modification.

Example:

cpp
// Base class - closed for modification
class PaymentProcessor {
public:
    virtual bool processPayment(double amount) = 0;
    virtual ~PaymentProcessor() = default;
};

// Extensions - open for extension
class CreditCardProcessor : public PaymentProcessor {
public:
    bool processPayment(double amount) override {
        // Credit card processing logic
        return true;
    }
};

class PayPalProcessor : public PaymentProcessor {
public:
    bool processPayment(double amount) override {
        // PayPal processing logic
        return true;
    }
};

class CryptoProcessor : public PaymentProcessor {
public:
    bool processPayment(double amount) override {
        // Cryptocurrency processing logic
        return true;
    }
};

Liskov Substitution Principle (LSP)

Derived classes should be substitutable for their base classes without affecting program correctness.

Bad example:

cpp
class Rectangle {
protected:
    int width, height;

public:
    virtual void setWidth(int w) { width = w; }
    virtual void setHeight(int h) { height = h; }
    int getArea() const { return width * height; }
};

class Square : public Rectangle {
public:
    void setWidth(int w) override {
        width = height = w;  // Violates LSP - changes both dimensions
    }

    void setHeight(int h) override {
        width = height = h;  // Violates LSP - changes both dimensions
    }
};

// This breaks LSP
void testArea(Rectangle& r) {
    r.setWidth(5);
    r.setHeight(4);
    assert(r.getArea() == 20);  // Fails for Square!
}

Good example:

cpp
class Shape {
public:
    virtual int getArea() const = 0;
    virtual ~Shape() = default;
};

class Rectangle : public Shape {
private:
    int width, height;

public:
    Rectangle(int w, int h) : width(w), height(h) {}
    int getArea() const override { return width * height; }
};

class Square : public Shape {
private:
    int side;

public:
    Square(int s) : side(s) {}
    int getArea() const override { return side * side; }
};

Interface Segregation Principle (ISP)

Clients should not be forced to depend on interfaces they don't use.

Bad example:

cpp
class Worker {
public:
    virtual void work() = 0;
    virtual void eat() = 0;
    virtual void sleep() = 0;
};

class Robot : public Worker {
public:
    void work() override { /* ... */ }
    void eat() override { /* Robot doesn't eat! */ }
    void sleep() override { /* Robot doesn't sleep! */ }
};

Good example:

cpp
class Workable {
public:
    virtual void work() = 0;
    virtual ~Workable() = default;
};

class Eatable {
public:
    virtual void eat() = 0;
    virtual ~Eatable() = default;
};

class Sleepable {
public:
    virtual void sleep() = 0;
    virtual ~Sleepable() = default;
};

class Human : public Workable, public Eatable, public Sleepable {
public:
    void work() override { /* ... */ }
    void eat() override { /* ... */ }
    void sleep() override { /* ... */ }
};

class Robot : public Workable {
public:
    void work() override { /* ... */ }
    // No need to implement eat() or sleep()
};

Dependency Inversion Principle (DIP)

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

Bad example:

cpp
class EmailNotifier {
public:
    void sendEmail(const std::string& message) { /* ... */ }
};

class OrderService {
private:
    EmailNotifier notifier;  // Depends on concrete class

public:
    void processOrder(Order& order) {
        // Process order...
        notifier.sendEmail("Order processed");
    }
};

Good example:

cpp
class NotificationService {
public:
    virtual void notify(const std::string& message) = 0;
    virtual ~NotificationService() = default;
};

class EmailNotifier : public NotificationService {
public:
    void notify(const std::string& message) override {
        // Send email
    }
};

class SMSNotifier : public NotificationService {
public:
    void notify(const std::string& message) override {
        // Send SMS
    }
};

class OrderService {
private:
    std::unique_ptr<NotificationService> notifier;

public:
    OrderService(std::unique_ptr<NotificationService> n) 
        : notifier(std::move(n)) {}

    void processOrder(Order& order) {
        // Process order...
        notifier->notify("Order processed");
    }
};

Composition vs Inheritance: The "Favor Composition" Rule

When to Use Inheritance

Good inheritance examples:

cpp
// True "is-a" relationship
class Vehicle {
public:
    virtual void start() = 0;
    virtual void stop() = 0;
};

class Car : public Vehicle {
public:
    void start() override { /* Start car engine */ }
    void stop() override { /* Stop car engine */ }
};

class Motorcycle : public Vehicle {
public:
    void start() override { /* Start motorcycle engine */ }
    void stop() override { /* Stop motorcycle engine */ }
};

When to Use Composition

Good composition examples:

cpp
// "Has-a" relationship
class Engine {
public:
    void start() { /* ... */ }
    void stop() { /* ... */ }
};

class Car {
private:
    Engine engine;
    std::vector<Wheel> wheels;

public:
    void start() {
        engine.start();
    }

    void stop() {
        engine.stop();
    }
};

Why favor composition:

  • Flexibility: Can change behavior at runtime
  • Loose coupling: Components are independent
  • Multiple behaviors: Can combine multiple behaviors
  • Testing: Easier to test individual components

Design Patterns

Strategy Pattern: Runtime Behavior Selection

cpp
class SortingStrategy {
public:
    virtual void sort(std::vector<int>& data) = 0;
    virtual ~SortingStrategy() = default;
};

class QuickSort : public SortingStrategy {
public:
    void sort(std::vector<int>& data) override {
        // Quick sort implementation
    }
};

class MergeSort : public SortingStrategy {
public:
    void sort(std::vector<int>& data) override {
        // Merge sort implementation
    }
};

class Sorter {
private:
    std::unique_ptr<SortingStrategy> strategy;

public:
    Sorter(std::unique_ptr<SortingStrategy> s) : strategy(std::move(s)) {}

    void setStrategy(std::unique_ptr<SortingStrategy> s) {
        strategy = std::move(s);
    }

    void sort(std::vector<int>& data) {
        strategy->sort(data);
    }
};

Observer Pattern: Event Notification

cpp
class Observer {
public:
    virtual void update(const std::string& message) = 0;
    virtual ~Observer() = default;
};

class Subject {
private:
    std::vector<Observer*> observers;

public:
    void attach(Observer* observer) {
        observers.push_back(observer);
    }

    void detach(Observer* observer) {
        // Remove observer
    }

    void notify(const std::string& message) {
        for (auto observer : observers) {
            observer->update(message);
        }
    }
};

class StockMarket : public Subject {
public:
    void priceChanged(const std::string& symbol, double price) {
        notify(symbol + " price changed to " + std::to_string(price));
    }
};

class TradingBot : public Observer {
public:
    void update(const std::string& message) override {
        // React to price changes
    }
};

Performance Considerations in OOP

Virtual Function Overhead

Virtual functions have a small performance cost due to indirection:

cpp
class Base {
public:
    virtual void method() { /* ... */ }  // Virtual function call
    void nonVirtual() { /* ... */ }      // Direct function call
};

class Derived : public Base {
public:
    void method() override { /* ... */ }
};

When virtual functions matter:

  • Hot paths: In performance-critical code
  • Frequent calls: When called millions of times
  • Tight loops: Inside performance-sensitive loops

Memory Layout and Cache Performance

cpp
// Good: Contiguous memory layout
class Point {
    double x, y, z;  // Contiguous in memory
};

std::vector<Point> points;  // Array of structures

// Less optimal: Scattered memory access
class Point {
    double* x, *y, *z;  // Pointers to scattered memory
};

The Bottom Line

Object-oriented design principles help create:

  • Maintainable code: Easy to understand and modify
  • Extensible systems: Easy to add new features
  • Reusable components: Code that can be used in multiple contexts
  • Testable code: Easy to test individual components

The key is applying these principles judiciously:

  • SOLID principles: Guide overall architecture
  • Composition over inheritance: Prefer "has-a" over "is-a" when possible
  • Design patterns: Use proven solutions to common problems
  • Performance awareness: Consider the performance implications of design choices

Remember: Good object-oriented design is about creating code that is easy to understand, modify, and extend. The principles are tools to help achieve these goals, not rules to be followed blindly.

Questions

Q: What does the 'S' in SOLID principles stand for?

Single Responsibility Principle states that a class should have only one reason to change.

Q: What is encapsulation in OOP?

Encapsulation is the bundling of data and methods that operate on that data, hiding internal state.

Q: When should you prefer composition over inheritance?

Composition is preferred when establishing 'has-a' relationships, while inheritance is for 'is-a' relationships.

Q: What is polymorphism?

Polymorphism allows treating objects of different types uniformly through a common interface.

Q: What is the Open/Closed Principle?

The Open/Closed Principle states that software entities should be open for extension but closed for modification.