🏗️ Object-Oriented Programming

Inheritance & Composition

Inheritance: "Is-A" Relationship

class Vehicle:
    def __init__(self, make: str, model: str):
        self.make = make
        self.model = model

class Car(Vehicle):
    def __init__(self, make: str, model: str, doors: int):
        super().__init__(make, model)
        self.doors = doors
class Vehicle {
protected:
    std::string make_;
    std::string model_;

public:
    Vehicle(std::string make, std::string model)
        : make_(std::move(make)), model_(std::move(model)) {}
};

class Car : public Vehicle {
    int doors_;

public:
    Car(std::string make, std::string model, int doors)
        : Vehicle(std::move(make), std::move(model)), doors_(doors) {}
};
public class Vehicle {
    protected final String make;
    protected final String model;

    public Vehicle(String make, String model) {
        this.make = make;
        this.model = model;
    }
}

public final class Car extends Vehicle {
    private final int doors;

    public Car(String make, String model, int doors) {
        super(make, model);
        this.doors = doors;
    }
}
public class Vehicle {
    protected string Make { get; }
    protected string Model { get; }

    public Vehicle(string make, string model) {
        Make = make;
        Model = model;
    }
}

public sealed class Car : Vehicle {
    public int Doors { get; }

    public Car(string make, string model, int doors)
        : base(make, model) {
        Doors = doors;
    }
}

When to Use Inheritance

Good fits: - Clear "is-a" relationship (Dog is an Animal) - Shared behavior + specialized extensions - Framework extension points

Avoid: - Code reuse only (use composition) - "Has-a" relationships - Deep hierarchies (>3 levels)

Composition: "Has-A" Relationship

class Engine:
    def start(self) -> None:
        print("Engine started")

class Car:
    def __init__(self):
        self._engine = Engine()  # Composition

    def start(self) -> None:
        self._engine.start()
class Engine {
public:
    void start() { std::cout << "Engine started\n"; }
};

class Car {
    Engine engine_;  // Composition

public:
    void start() { engine_.start(); }
};
public final class Engine {
    public void start() { System.out.println("Engine started"); }
}

public final class Car {
    private final Engine engine = new Engine();

    public void start() { engine.start(); }
}
public sealed class Engine {
    public void Start() => Console.WriteLine("Engine started");
}

public sealed class Car {
    private readonly Engine _engine = new Engine();

    public void Start() => _engine.Start();
}

Composition over Inheritance

Aspect Inheritance Composition
Coupling Tight (parent changes break child) Loose
Flexibility Fixed at compile time Runtime swappable
Reusability Library-specific Any object with interface
Testing Harder to mock Easy to mock/inject

The "Favor Composition" Rule

"Prefer composition over inheritance" — GoF Design Patterns

# Instead of inheritance:
class LoggingDatabase(Database):
    def query(self, sql: str) -> None:
        log(sql)
        super().query(sql)

# Prefer composition:
class Database:
    def __init__(self, logger=None):
        self.logger = logger

    def query(self, sql: str) -> None:
        if self.logger:
            self.logger.log(sql)
        # execute query
// Instead of inheritance:
class LoggingDatabase : public Database {
public:
    void query(const std::string& sql) override {
        log(sql);
        Database::query(sql);
    }
};

// Prefer composition:
class Database {
    Logger* logger_;
public:
    explicit Database(Logger* logger = nullptr) : logger_(logger) {}

    void query(const std::string& sql) {
        if (logger_) logger_->log(sql);
        // execute query
    }
};
// Instead of inheritance:
class LoggingDatabase extends Database {
    @Override
    public void query(String sql) {
        log(sql);
        super.query(sql);
    }
}

// Prefer composition:
public final class Database {
    private final Logger logger;

    public Database(Logger logger) { this.logger = logger; }

    public void query(String sql) {
        if (logger != null) logger.log(sql);
        // execute query
    }
}
// Instead of inheritance:
class LoggingDatabase : Database {
    public override void Query(string sql) {
        Log(sql);
        base.Query(sql);
    }
}

// Prefer composition:
public sealed class Database {
    private readonly ILogger _logger;

    public Database(ILogger logger) => _logger = logger;

    public void Query(string sql) {
        _logger?.Log(sql);
        // execute query
    }
}

The Liskov Substitution Principle (LSP)

If S is a subtype of T, objects of T must be replaceable with objects of S without breaking the program.

Classic Violation: Square/Rectangle

class Rectangle:
    def __init__(self, width: int, height: int):
        self.width = width
        self.height = height

    def area(self) -> int:
        return self.width * self.height

class Square(Rectangle):
    def __init__(self, size: int):
        super().__init__(size, size)

    @property
    def width(self) -> int:
        return self._width

    @width.setter
    def width(self, value: int) -> None:
        self._width = self._height = value

    @property
    def height(self) -> int:
        return self._height

    @height.setter
    def height(self, value: int) -> None:
        self._width = self._height = value
class Rectangle {
protected:
    int width_;
    int height_;

public:
    Rectangle(int w, int h) : width_(w), height_(h) {}
    virtual int width() const { return width_; }
    virtual int height() const { return height_; }
    virtual void width(int w) { width_ = w; }
    virtual void height(int h) { height_ = h; }
    int area() const { return width_ * height_; }
};

class Square : public Rectangle {
public:
    Square(int size) : Rectangle(size, size) {}
    void width(int w) override { width_ = height_ = w; }
    void height(int h) override { width_ = height_ = h; }
};
public class Rectangle {
    protected int width, height;

    public Rectangle(int w, int h) { width = w; height = h; }
    public int getWidth() { return width; }
    public int getHeight() { return height; }
    public void setWidth(int w) { width = w; }
    public void setHeight(int h) { height = h; }
    public int area() { return width * height; }
}

public class Square extends Rectangle {
    public Square(int size) { super(size, size); }
    @Override public void setWidth(int w) { width = height = w; }
    @Override public void setHeight(int h) { width = height = h; }
}
public class Rectangle {
    protected int Width { get; set; }
    protected int Height { get; set; }

    public Rectangle(int w, int h) { Width = w; Height = h; }
    public virtual int Area() => Width * Height;
}

public class Square : Rectangle {
    public Square(int size) : base(size, size) {}
    public override int Width { set => Width = Height = value; }
    public override int Height { set => Width = Height = value; }
}
def resize(rect: Rectangle) -> None:
    rect.width = 10
    rect.height = 5
    assert rect.area() == 50  # FAILS for Square! 10×10=100
void resize(Rectangle& rect) {
    rect.width(10);
    rect.height(5);
    assert(rect.area() == 50);  // FAILS for Square! 10×10=100
}
void resize(Rectangle rect) {
    rect.setWidth(10);
    rect.setHeight(5);
    assert rect.area() == 50;  // FAILS for Square! 10×10=100
}
void Resize(Rectangle rect) {
    rect.Width = 10;
    rect.Height = 5;
    Debug.Assert(rect.Area() == 50); // FAILS for Square! 10×10=100
}

Fix: Don't inherit Square from Rectangle. Use composition or make Square a factory function.

The Strategy Pattern (Composition in Action)

from abc import ABC, abstractmethod

class CompressionStrategy(ABC):
    @abstractmethod
    def compress(self, data: str) -> str:
        pass

class ZipStrategy(CompressionStrategy):
    def compress(self, data: str) -> str:
        return f"zip:{data}"

class GzipStrategy(CompressionStrategy):
    def compress(self, data: str) -> str:
        return f"gzip:{data}"

class Compressor:
    def __init__(self, strategy: CompressionStrategy):
        self.strategy = strategy

    def compress(self, data: str) -> str:
        return self.strategy.compress(data)

    def set_strategy(self, strategy: CompressionStrategy) -> None:
        self.strategy = strategy

# Usage — swap at runtime!
c = Compressor(ZipStrategy())
print(c.compress("data"))  # "zip:data"
c.set_strategy(GzipStrategy())
print(c.compress("data"))  # "gzip:data"
#include <memory>
#include <string>

class CompressionStrategy {
public:
    virtual std::string compress(const std::string& data) = 0;
    virtual ~CompressionStrategy() = default;
};

class ZipStrategy : public CompressionStrategy {
    std::string compress(const std::string& data) override { return "zip:" + data; }
};

class GzipStrategy : public CompressionStrategy {
    std::string compress(const std::string& data) override { return "gzip:" + data; }
};

class Compressor {
    std::unique_ptr<CompressionStrategy> strategy_;

public:
    explicit Compressor(std::unique_ptr<CompressionStrategy> s) : strategy_(std::move(s)) {}
    std::string compress(const std::string& data) { return strategy_->compress(data); }
    void setStrategy(std::unique_ptr<CompressionStrategy> s) { strategy_ = std::move(s); }
};

// Usage — swap at runtime!
Compressor c(std::make_unique<ZipStrategy>());
c.compress("data");  // "zip:data"
c.setStrategy(std::make_unique<GzipStrategy>());
c.compress("data");  // "gzip:data"
interface CompressionStrategy {
    String compress(String data);
}

class ZipStrategy implements CompressionStrategy {
    public String compress(String data) { return "zip:" + data; }
}

class GzipStrategy implements CompressionStrategy {
    public String compress(String data) { return "gzip:" + data; }
}

public final class Compressor {
    private CompressionStrategy strategy;

    public Compressor(CompressionStrategy s) { this.strategy = s; }
    public String compress(String data) { return strategy.compress(data); }
    public void setStrategy(CompressionStrategy s) { this.strategy = s; }
}

// Usage — swap at runtime!
Compressor c = new Compressor(new ZipStrategy());
c.compress("data");  // "zip:data"
c.setStrategy(new GzipStrategy());
c.compress("data");  // "gzip:data"
public interface ICompressionStrategy {
    string Compress(string data);
}

public sealed class ZipStrategy : ICompressionStrategy {
    public string Compress(string data) => "zip:" + data;
}

public sealed class GzipStrategy : ICompressionStrategy {
    public string Compress(string data) => "gzip:" + data;
}

public sealed class Compressor {
    private ICompressionStrategy _strategy;

    public Compressor(ICompressionStrategy s) => _strategy = s;
    public string Compress(string data) => _strategy.Compress(data);
    public void SetStrategy(ICompressionStrategy s) => _strategy = s;
}

// Usage — swap at runtime!
var c = new Compressor(new ZipStrategy());
c.Compress("data");  // "zip:data"
c.SetStrategy(new GzipStrategy());
c.Compress("data");  // "gzip:data"