# Single Responsibility in OOP and Composition Versus Inheritance<no value>

## What will we learn?

- The **Single Responsibility Principle (SRP)** at the level of a Class.
- **Composition**: an object made of other objects.
- **Composition versus Inheritance**: how to choose.

## Why do we need this?

In the clean code topic we learned SRP for **functions**: every function does one thing. Exactly the same principle applies to **Classes**, and there it is even more important, because a big Class is a "pile" of data **and** behavior. When every change (in the receipt format, in the payment method, in the VAT calculation) touches the same Class, it becomes fragile and scary to change.

And in the lesson on inheritance we saw that a car should **not** inherit from an engine. Today we will see what you **do** do.

## Explanation from the basics

### SRP at the level of a Class

**Every Class should have one reason to change** (Responsibility = "a reason for change").

**How do you recognize a Class with too much responsibility?**

1. It is hard to describe it in one sentence **without "and also"**.
2. It has **groups of Attributes** where each group is used only by part of the methods.
3. It changes for **different** reasons: a change in the receipt design, and a change in the tax rules, and a change in sending email.
4. Its name is too general: `Manager`, `Handler`, `Processor`, `Utils`, `System`.

### Composition — "has a" (has-a)

**Composition** = building a complex object **out of other objects**, each one responsible for its own part. The complex object **holds** them as Attributes, and **delegates** work to them.

```python
class Car:
    def __init__(self):
        self.engine = Engine()       # the car HAS an engine
        self.gps = GPS()             # the car HAS a GPS

    def start(self):
        return self.engine.start()   # delegate the work to the part that knows how
```

{{< box "analogy" "Analogy: a computer" >}}
A computer is **made of** a processor, memory, a disk and a graphics card (remember the topic on the computer and the operating system!). The computer does not "inherit" from the processor. It **has** a processor. Every component is responsible for one job, and you can **replace** a component (a bigger disk) without changing the rest. That is Composition.
{{< /box >}}

### Composition versus Inheritance

| | Inheritance | Composition |
|---|---|---|
| Relationship | **is-a** ("is a kind of") | **has-a** ("has a") |
| Example | `Manager(Employee)` | `Car` holds an `Engine` |
| Coupling | Tight. The Child depends on all the details of the Parent | Loose. You talk only through the part's interface |
| Flexibility | Decided when the Class is written | You can replace a part **at runtime** |
| When something changes in the Parent/the part | It may break all the children | It affects only whoever uses the interface |

**An accepted rule of thumb in the industry: "Favor Composition over Inheritance"** — prefer Composition. Use inheritance **only** when there is a real is-a relationship, and when you want Polymorphism.

**The simple test:**

- "X **is a kind of** Y"? → inheritance.
- "X **has** a Y"? / "X **uses** a Y"? → Composition.

## Syntax

```python
class Part:
    def do_work(self):
        return "part did its job"

class Whole:
    def __init__(self, part):
        self.part = part                 # composition: hold a part object

    def work(self):
        return self.part.do_work()       # delegation: let the part do it

whole = Whole(Part())                    # the part is passed in (can be swapped)
```

When the Part is passed **as a parameter** (and not created inside), you can pass different parts, and that is the place where Composition and Polymorphism meet.

## First code example

**A God Class before breaking it apart:**

```python
class ShopSystem:
    def __init__(self):
        self.items = []
        self.vat = 0.17
        self.customer_email = ""

    def add_item(self, name, price, quantity):
        self.items.append((name, price, quantity))

    def subtotal(self):
        return sum(price * quantity for name, price, quantity in self.items)

    def total_with_vat(self):
        return round(self.subtotal() * (1 + self.vat), 2)

    def receipt_text(self):
        lines = []
        for name, price, quantity in self.items:
            lines.append(name + " x" + str(quantity) + " = " + str(price * quantity))
        lines.append("TOTAL: " + str(self.total_with_vat()))
        return "\n".join(lines)

    def send_receipt(self):
        print("Sending email to", self.customer_email)
        print(self.receipt_text())
```

**How many reasons for change are there here?** (1) How the items in the cart are managed. (2) How tax is calculated. (3) What a receipt looks like. (4) How it is sent. **Four responsibilities** in one Class.

**After breaking it apart with Composition.** `week-3/shop_composition.py`:

```python
class Cart:
    def __init__(self):
        self._items = []

    def add(self, name, price, quantity=1):
        if price < 0 or quantity <= 0:
            raise ValueError("invalid price or quantity")
        self._items.append((name, price, quantity))

    def items(self):
        return list(self._items)

    def subtotal(self):
        return sum(price * quantity for name, price, quantity in self._items)

class TaxCalculator:
    def __init__(self, rate):
        self.rate = rate

    def total(self, amount):
        return round(amount * (1 + self.rate), 2)

class ReceiptFormatter:
    def format(self, cart, total):
        lines = []
        for name, price, quantity in cart.items():
            lines.append(name + " x" + str(quantity) + " = " + str(price * quantity))
        lines.append("TOTAL: " + str(total))
        return "\n".join(lines)

class Checkout:
    def __init__(self, cart, tax, formatter):
        self.cart = cart
        self.tax = tax
        self.formatter = formatter

    def receipt(self):
        total = self.tax.total(self.cart.subtotal())
        return self.formatter.format(self.cart, total)

cart = Cart()
cart.add("Bread", 8, 2)
cart.add("Cheese", 32)

checkout = Checkout(cart, TaxCalculator(0.17), ReceiptFormatter())
print(checkout.receipt())
```

**Output:**

```text
Bread x2 = 16
Cheese x1 = 32
TOTAL: 56.16
```

## Breaking down the code line by line

| Class | One responsibility | One reason for change |
|---|---|---|
| `Cart` | Managing the items | The cart's rules (quantities, limits) |
| `TaxCalculator` | Calculating tax | A change in the tax law |
| `ReceiptFormatter` | Formatting a receipt as text | A change in the receipt design |
| `Checkout` | **Connecting** the parts | A change in the checkout process |

- `Checkout` is **made of** a cart, a tax calculator and a formatter (**Composition**), and **delegates** the work to them: `self.tax.total(...)`, `self.formatter.format(...)`. It hardly calculates anything itself.
- Every Class is small, clear, and can be tested on its own: `TaxCalculator(0.17).total(100)` → `117.0`.

## Another example

**The power of Composition: replacing parts** (Composition + Polymorphism). We add a different receipt formatter, and a tax calculator for an exempt business. **`Checkout` does not change at all.**

```python
class HtmlReceiptFormatter:
    def format(self, cart, total):
        rows = ""
        for name, price, quantity in cart.items():
            rows = rows + "<li>" + name + " x" + str(quantity) + "</li>"
        return "<ul>" + rows + "</ul><b>Total: " + str(total) + "</b>"

class TaxExempt:
    def total(self, amount):
        return amount

html_checkout = Checkout(cart, TaxCalculator(0.17), HtmlReceiptFormatter())
print(html_checkout.receipt())

exempt_checkout = Checkout(cart, TaxExempt(), ReceiptFormatter())
print(exempt_checkout.receipt())
```

**Output:**

```text
<ul><li>Bread x2</li><li>Cheese x1</li></ul><b>Total: 56.16</b>
Bread x2 = 16
Cheese x1 = 32
TOTAL: 48
```

**Compare with the God Class version:** to support an HTML receipt and a tax-exempt business, we would have had to add `if`s inside `receipt_text` and `total_with_vat`. Here **every option is a part you swap**.

### Composition instead of inheritance — the classic example

```python
class Engine:
    def __init__(self, horsepower):
        self.horsepower = horsepower
        self.running = False

    def start(self):
        self.running = True
        return "Engine started (" + str(self.horsepower) + " hp)"

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

    def start(self):
        return self.model + ": " + self.engine.start()

    def upgrade_engine(self, new_engine):
        self.engine = new_engine

car = Car("Corolla", Engine(130))
print(car.start())
car.upgrade_engine(Engine(180))
print(car.start())
```

```text
Corolla: Engine started (130 hp)
Corolla: Engine started (180 hp)
```

With **inheritance** (`class Car(Engine)`), you cannot "replace the engine". The car **is** the engine. With **Composition**, you replace a part at runtime.

## Common mistake

**Mistake 1: inheritance in order to "get useful methods".**

```python
class Order(list):
    pass

order = Order()
order.append("pizza")
order.sort()
order.clear()        # an order that can be "cleared", "reversed", "popped"...?
```

`Order` got **all** 40+ methods of `list`, including ones that have no meaning for an order, and you cannot control the rules. The right way: **Composition**, `self._items = []`, and exposing **only** what an order needs (`add`, `items`).

**Mistake 2: an "exposed" Composition — bypassing the complex object.**

```python
checkout.cart._items.append(("Free TV", -500, 1))
```

Deep chaining that touches the inside of a part bypasses its rules. Talk to a part **only through its public methods** (`checkout.cart.add(...)`).

**Mistake 3: over-splitting.** 15 Classes of 3 lines for a problem that is solved with 2 clear Classes. SRP says "one reason for change", not "one method per Class". If two actions **always** change together, they are probably one responsibility.

**Mistake 4: a "Composition" where the part creates itself inside, always the same.** `self.tax = TaxCalculator(0.17)` inside `__init__` works, but then you cannot replace it. Passing the part as a parameter (`Checkout(cart, tax, formatter)`) gives the flexibility.

<!-- exercises:start — generated by scripts/intro6/sync_site_exercises.py; edit scripts/intro6/exercises/ -->
## Practice

The exercises use only what was taught in this lesson and the lessons before it. Try on your own first, and only then open the solution.

**What you practice on this page:** one responsibility per Class (SRP), Composition — a Class that holds other objects, replacing a part without changing the rest, and when not to use inheritance.

**By the end of the page you will know how to:**
- Recognize a Class that does too much and break it apart
- Build a Class made of other objects (has-a)
- Replace a component in an object without changing the code that uses it
- Tell apart "is a kind of" (inheritance) and "has a" (composition)

### Exercise 1: A Class that does everything

**What you do:** design only — recognize a Class that does too much, and suggest a split.

**The code:**

```text
class ShopSystem:
    def add_item(self, name, price, quantity): ...
    def subtotal(self): ...
    def add_vat(self, amount): ...
    def format_receipt(self): ...
    def send_email(self, address): ...
```

**The task:**
1. How many different responsibilities are there here? Write each one in words.
2. Suggest a split into several Classes: a name for each Class, and which methods move to it.

**What to submit:** the list of responsibilities, and a table of Classes and methods.

{{< solution >}}
1. Four responsibilities: **managing the cart** (items and total), **calculating tax**, **formatting a receipt**, **sending email**. Each one is a different "reason to change" — this is a **God Class**.
2. A split:

| Class | Responsibility | Methods |
|---|---|---|
| `Cart` | The items | `add_item`, `subtotal` |
| `VatCalculator` | The tax | `add_vat` |
| `ReceiptFormatter` | The formatting | `format_receipt` |
| `EmailSender` | The sending | `send_email` |
{{< /solution >}}

### Exercise 2: Composition — the cart and the checkout

**What you build:** a shopping cart and a checkout — where the checkout is made of a cart and a tax calculator.

**What to write:**
1. `Cart` — with `add` that receives a name, a price and a quantity (default 1), and `subtotal` that returns the cart's total.
2. `IsraeliVat` — with `apply` that receives an amount and returns it including 17% VAT.
3. `Checkout` — receives a cart and a tax calculator in `__init__` and holds them (has-a).
4. In `Checkout`: `total` — the cart's total, after the tax calculator.

**Test code:**

```python
cart = Cart()
cart.add("Pen", 5, 4)
cart.add("Bag", 60)
checkout = Checkout(cart, IsraeliVat())
print(checkout.total())
```

**The desired output:**

```text
93.6
```

{{< solution >}}
```python
class Cart:
    def __init__(self):
        self._items = []

    def add(self, name, price, quantity=1):
        self._items.append((name, price, quantity))

    def subtotal(self):
        total = 0
        for name, price, quantity in self._items:
            total = total + price * quantity
        return total


class IsraeliVat:
    RATE = 0.17

    def apply(self, amount):
        return round(amount * (1 + IsraeliVat.RATE), 2)


class Checkout:
    def __init__(self, cart, tax):
        self.cart = cart
        self.tax = tax

    def total(self):
        return self.tax.apply(self.cart.subtotal())


cart = Cart()
cart.add("Pen", 5, 4)
cart.add("Bag", 60)
checkout = Checkout(cart, IsraeliVat())
print(checkout.total())
```

```text
93.6
```

`Checkout` does not calculate by itself — it **delegates** (Delegation) to the cart and to the tax calculator, each of which is an expert in one thing.
{{< /solution >}}

### Exercise 3: Replacing a part

**What you build:** continuing the checkout — replacing the tax calculator without changing `Checkout`.

**What to write:**
1. A new Class named `TaxExempt`, with `apply` that returns the amount unchanged.
2. Create one checkout with `IsraeliVat` and a second checkout with `TaxExempt`, on the same cart, and print the total of both.
3. Check: you did not change a single line in `Checkout`.

**The desired output:**

```text
93.6
80
```

{{< solution >}}
```python
class Cart:
    def __init__(self):
        self._items = []

    def add(self, name, price, quantity=1):
        self._items.append((name, price, quantity))

    def subtotal(self):
        total = 0
        for name, price, quantity in self._items:
            total = total + price * quantity
        return total


class IsraeliVat:
    RATE = 0.17

    def apply(self, amount):
        return round(amount * (1 + IsraeliVat.RATE), 2)


class TaxExempt:
    def apply(self, amount):
        return amount


class Checkout:
    def __init__(self, cart, tax):
        self.cart = cart
        self.tax = tax

    def total(self):
        return self.tax.apply(self.cart.subtotal())


cart = Cart()
cart.add("Pen", 5, 4)
cart.add("Bag", 60)
print(Checkout(cart, IsraeliVat()).total())
print(Checkout(cart, TaxExempt()).total())
```

```text
93.6
80
```

This is the power of **Composition + Polymorphism**: every tax calculator implements `apply`, and the checkout works with any of them.
{{< /solution >}}

### Exercise 4: Inheriting to save code?

**What you do:** recognize wrong inheritance, and fix it with composition.

**The code:**

```text
class Engine:
    def start(self):
        return "engine started"

class Car(Engine):
    def __init__(self, model):
        self.model = model
```

**The task:**
1. Why is the inheritance here wrong, even though the code works? Use the "is a kind of" test.
2. Rewrite: `Engine` receives horsepower, and `Car` receives a model and an engine and holds it (has-a). `Car` has a `start` that uses the engine.

**Test code:**

```python
car = Car("Mazda", Engine(150))
print(car.start())
```

**The desired output:**

```text
Mazda: engine 150hp started
```

{{< solution >}}
1. A car **is not a kind of** engine — a car **has** an engine. They inherited only to "get" `start`, and now `Car` includes everything an engine has, and you cannot replace the engine.
2. **has-a**:

```python
class Engine:
    def __init__(self, horsepower):
        self.horsepower = horsepower

    def start(self):
        return "engine " + str(self.horsepower) + "hp started"


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

    def start(self):
        return self.model + ": " + self.engine.start()


car = Car("Mazda", Engine(150))
print(car.start())
```

```text
Mazda: engine 150hp started
```
{{< /solution >}}

### Exercise 5: A computer from parts

**What you build:** a computer made of a processor, memory and a disk.

**What to write:**
1. `CPU` — with a number of cores and a speed in GHz.
2. `RAM` — with a size in GB.
3. `Disk` — with a size in GB.
4. `Computer` — receives the three parts and holds them. It has a `__str__` that describes the computer as in the output, and `can_run` that receives a minimum of cores and a minimum of memory and returns `True` or `False`.

**Test code:**

```python
pc = Computer(CPU(8, 3.2), RAM(8), Disk(512))
print(pc)
print("Can run the game?", pc.can_run(6, 16))
```

**The desired output:**

```text
8-core 3.2GHz CPU, 8GB RAM, 512GB SSD
Can run the game? False
```

{{< solution >}}
```python
class CPU:
    def __init__(self, cores, ghz):
        self.cores = cores
        self.ghz = ghz

    def __str__(self):
        return str(self.cores) + "-core " + str(self.ghz) + "GHz CPU"


class RAM:
    def __init__(self, gb):
        self.gb = gb

    def __str__(self):
        return str(self.gb) + "GB RAM"


class Disk:
    def __init__(self, gb):
        self.gb = gb

    def __str__(self):
        return str(self.gb) + "GB SSD"


class Computer:
    def __init__(self, cpu, ram, disk):
        self.cpu = cpu
        self.ram = ram
        self.disk = disk

    def can_run(self, min_cores, min_ram):
        return self.cpu.cores >= min_cores and self.ram.gb >= min_ram

    def __str__(self):
        return str(self.cpu) + ", " + str(self.ram) + ", " + str(self.disk)


pc = Computer(CPU(8, 3.2), RAM(8), Disk(512))
print(pc)
print("Can run the game?", pc.can_run(6, 16))
```

```text
8-core 3.2GHz CPU, 8GB RAM, 512GB SSD
Can run the game? False
```

There are enough cores (8 ≥ 6), but only 8GB of RAM out of the 16 required. Every part knows how to present itself, and `Computer` only connects them.
{{< /solution >}}

<!-- exercises:end -->

## Summary

- **SRP in a Class**: one reason to change. A God Class (`...Manager`, `...System`) → break it apart.
- **Composition** = an object made of other objects (**has-a**), which **delegates** work to them.
- **Inheritance** only for a real **is-a**. For has-a or "uses-a" → Composition.
- **Favor Composition over Inheritance**: loose coupling, and parts that can be replaced.
- Pass parts **as parameters** to `__init__`, so that they can be replaced (Composition + Polymorphism).
- Talk to a part **through its interface**, not through its insides.

<!-- docs:start — generated by scripts/intro6/site_docs.py; edit the links there -->

## Official documentation

- [Python Tutorial — Classes](https://docs.python.org/3/tutorial/classes.html)
- [Python Tutorial — Inheritance](https://docs.python.org/3/tutorial/classes.html#inheritance)

<!-- docs:end -->
