# Inheritance vs 🧩 Composition in Object-Oriented Programming

When designing software systems using Object-Oriented Programming (OOP), two powerful techniques to organize and reuse code are **inheritance** and **composition**. While both achieve code reuse, they differ significantly in how they model relationships between objects.

Understanding the difference is crucial for building clean, maintainable, and scalable software. Let’s dive into their definitions, use-cases, and differences.

---

## 📘 What is Inheritance?

**Inheritance** is when one class (the **child/subclass**) derives from another class (the **parent/base class**) and **inherits its attributes and methods**.

### 🔗 Relationship: "Is-a"

If `Dog` inherits from `Animal`, then a **Dog is an Animal**.

### ✅ Example:

```python
class Animal:
    def speak(self):
        print("Animal speaks")

class Dog(Animal):
    def speak(self):
        print("Dog barks")

d = Dog()
d.speak()
```

### 💡 Output:

```plaintext
Dog barks
```

Here, `Dog` overrides the `speak()` method of `Animal`, demonstrating **method overriding**.

---

## 🧩 What is Composition?

**Composition** means that one class contains an instance of another class and **delegates work to it**. Instead of inheriting, it uses the capabilities of other classes by **composing** them together.

### 🔗 Relationship: "Has-a"

If `Car` contains an `Engine`, then a **Car has an Engine**.

### ✅ Example:

```python
class Engine:
    def start(self):
        print("Engine starts")

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

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

c = Car()
c.start()
```

### 💡 Output:

```plaintext
Engine starts
```

---

## 🔍 Inheritance vs Composition: Head-to-Head

| Feature | Inheritance | Composition |
| --- | --- | --- |
| **Definition** | Derives a class from another | Combines behaviors by containing other objects |
| **Relationship Type** | "Is-a" | "Has-a" |
| **Coupling** | Tight coupling to base class | Loose coupling, flexible components |
| **Code Reuse** | Through class hierarchy | Through object collaboration |
| **Flexibility** | Less flexible; base changes ripple to children | More flexible; easier to swap or modify parts |
| **Best Used When** | Objects share strong common behavior | You want to delegate behavior or plug in reusable components |
| **Example Keyword** | `class B(A):` | `self.obj = SomeOtherClass()` |

---

## ⚠️ Real-World Scenarios

### 🧬 Inheritance:

Use when there's a **clear** **type hierarchy**.

```python
class Person:
    def get_id(self):
        return "Generic ID"

class Employee(Person):
    def get_id(self):
        return "Employee ID"
```

### 🧩 Composition:

Use when you want to **plug in behaviors** or **combine functionalities**.

```python
class Logger:
    def log(self, msg):
        print(f"Log: {msg}")

class Service:
    def __init__(self):
        self.logger = Logger()

    def run(self):
        self.logger.log("Service started")
```

## 🧠 When to Use Which?

| Use Inheritance When | Use Composition When |
| --- | --- |
| There's a clear "is-a" relationship | You want flexibility and modularity |
| You want to extend a base class’s functionality | You want to combine behavior from different classes |
| Code reuse through polymorphism makes sense | You want to favor delegation over tight coupling |

---

## 🏁 Conclusion

Both **inheritance** and **composition** are foundational to OOP, but serve different purposes.

* Use **inheritance** for **strongly related types** that share behavior.
    
* Use **composition** when building **modular, flexible systems** that need to evolve easily.
    

In modern software design, especially in large systems, **composition is often preferred** — summarized by the principle:

> **“Favor composition over inheritance.”**
