# Single Responsibility ב-OOP ו-Composition לעומת Inheritance<no value>

## מה נלמד?

- **Single Responsibility Principle (SRP)** ברמת ה-Class.
- **Composition** (הרכבה): אובייקט שמורכב מאובייקטים אחרים.
- **Composition מול Inheritance**: איך בוחרים.

## למה צריך את זה?

בנושא Python למדנו SRP ל**פונקציות**: כל פונקציה עושה דבר אחד. אותו עיקרון בדיוק חל על **Classes**, ושם הוא אפילו חשוב יותר, כי Class גדולה היא "ערימה" של נתונים **ו**התנהגות. כשכל שינוי (בפורמט הקבלה, בשיטת התשלום, בחישוב המע"מ) נוגע באותה Class, היא נהיית שבירה ומפחידה לשינוי.

ובשיעור על ירושה ראינו שמכונית **לא** צריכה לרשת ממנוע. היום נראה מה **כן** עושים.

## הסבר מהבסיס

### SRP ברמת Class

**לכל Class צריכה להיות סיבה אחת להשתנות** (Responsibility = "סיבה לשינוי").

**איך מזהים Class עם יותר מדי אחריות?**

1. קשה לתאר אותה במשפט אחד **בלי "וגם"**.
2. יש בה **קבוצות של Attributes** שכל קבוצה משמשת רק חלק מהמתודות.
3. היא משתנה מסיבות **שונות**: שינוי בעיצוב הקבלה, ושינוי בכללי המס, ושינוי בשליחת מייל.
4. השם שלה כללי מדי: `Manager`, `Handler`, `Processor`, `Utils`, `System`.

### Composition — "יש לו" (has-a)

**Composition** = בניית אובייקט מורכב **מתוך אובייקטים אחרים**, שכל אחד אחראי לחלק שלו. האובייקט המורכב **מחזיק** אותם כ-Attributes, ו**מאציל** (Delegate) להם עבודה.

```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" "אנלוגיה: מחשב" >}}
מחשב **מורכב** ממעבד, זיכרון, דיסק וכרטיס מסך (נושא מחשב, מערכת הפעלה ורשת!). המחשב לא "יורש" מהמעבד. **יש לו** מעבד. כל רכיב אחראי לתפקיד אחד, ואפשר **להחליף** רכיב (דיסק גדול יותר) בלי לשנות את השאר. זו Composition.
{{< /box >}}

### Composition מול Inheritance

| | Inheritance | Composition |
|---|---|---|
| יחס | **is-a** ("הוא סוג של") | **has-a** ("יש לו") |
| דוגמה | `Manager(Employee)` | `Car` מחזיקה `Engine` |
| קשר | הדוק. Child תלויה בכל הפרטים של Parent | רופף. מדברים רק דרך הממשק של החלק |
| גמישות | נקבע כשכותבים את ה-Class | אפשר להחליף חלק **בזמן ריצה** |
| כשמשהו משתנה ב-Parent/בחלק | עלול לשבור את כל הילדים | משפיע רק על מי שמשתמש בממשק |

**כלל אצבע מקובל בתעשייה: "Favor Composition over Inheritance"** — העדיפו Composition. השתמשו בירושה **רק** כשיש יחס is-a אמיתי, וכשרוצים Polymorphism.

**המבחן הפשוט:**

- "X **הוא סוג של** Y"? ‏←‏ ירושה.
- "ל-X **יש** Y"? / "X **משתמש ב**-Y"? → Composition.

## תחביר

```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)
```

כשה-Part מועבר **כפרמטר** (ולא נוצר בפנים), אפשר להעביר חלקים שונים, וזה המקום שבו Composition ו-Polymorphism נפגשים.

## דוגמת קוד ראשונה

**God Class לפני פירוק:**

```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())
```

**כמה סיבות לשינוי יש כאן?** (1) איך מנהלים פריטים בעגלה. (2) איך מחשבים מס. (3) איך נראית קבלה. (4) איך שולחים אותה. **ארבע אחריויות** ב-Class אחת.

**אחרי פירוק עם 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())
```

**פלט:**

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

## פירוק הקוד שורה אחר שורה

| Class | אחריות אחת | סיבה אחת לשינוי |
|---|---|---|
| `Cart` | לנהל את הפריטים | חוקי העגלה (כמויות, מגבלות) |
| `TaxCalculator` | לחשב מס | שינוי בחוק המס |
| `ReceiptFormatter` | לעצב קבלה כטקסט | שינוי בעיצוב הקבלה |
| `Checkout` | **לחבר** בין החלקים | שינוי בתהליך הקופה |

- `Checkout` **מורכבת** מעגלה, מחשבון מס ומעצב (**Composition**), ו**מאצילה** להם את העבודה: `self.tax.total(...)`, `self.formatter.format(...)`. היא עצמה כמעט לא מחשבת כלום.
- כל Class קטנה, ברורה, ואפשר לבדוק אותה לבד: `TaxCalculator(0.17).total(100)` → `117.0`.

## דוגמה נוספת

**הכוח של Composition: החלפת חלקים** (Composition + Polymorphism). מוסיפים מעצב קבלה אחר, ומחשבון מס לעסק פטור. **`Checkout` לא משתנה בכלל.**

```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())
```

**פלט:**

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

**השוו לגרסת ה-God Class:** כדי לתמוך בקבלה ב-HTML ובעסק פטור ממס, היינו צריכים להוסיף `if`-ים בתוך `receipt_text` ו-`total_with_vat`. כאן **כל אפשרות היא חלק שמחליפים**.

### Composition במקום ירושה — הדוגמה הקלאסית

```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)
```

עם **ירושה** (`class Car(Engine)`), אי אפשר "להחליף מנוע". המכונית **היא** המנוע. עם **Composition**, מחליפים חלק בזמן ריצה.

## טעות נפוצה

**הטעות 1: ירושה בשביל "לקבל מתודות שימושיות".**

```python
class Order(list):
    pass


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

`Order` קיבלה את **כל** 40+ המתודות של `list`, כולל כאלה שאין להן משמעות להזמנה, ואי אפשר לשלוט בכללים. הנכון: **Composition**, `self._items = []`, וחשיפה **רק** של מה שהזמנה צריכה (`add`, `items`).

**הטעות 2: Composition "חשופה" — לעקוף את האובייקט המורכב.**

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

שרשור עמוק שנוגע בפנים של חלק עוקף את הכללים שלו. מדברים עם חלק **רק דרך המתודות הציבוריות שלו** (`checkout.cart.add(...)`).

**הטעות 3: פירוק יתר.** 15 Classes של 3 שורות לבעיה שנפתרת ב-2 Classes ברורות. SRP אומר "סיבה אחת לשינוי", ולא "מתודה אחת ל-Class". אם שתי פעולות **תמיד** משתנות יחד, הן כנראה אחריות אחת.

**הטעות 4: "Composition" שבה החלק יוצר את עצמו בפנים, תמיד אותו דבר.** `self.tax = TaxCalculator(0.17)` בתוך `__init__` עובד, אבל אז אי אפשר להחליף. העברת החלק כפרמטר (`Checkout(cart, tax, formatter)`) נותנת את הגמישות.

<!-- exercises:start — generated by scripts/intro6/sync_site_exercises.py; edit scripts/intro6/exercises/ -->
## תרגול

התרגילים מבוססים רק על מה שנלמד בשיעור הזה ובשיעורים שלפניו. נסו לבד, ורק אז פתחו את הפתרון.

**מה מתרגלים בדף הזה:** אחריות אחת לכל Class (SRP), הרכבה (Composition) — Class שמחזיקה אובייקטים אחרים, החלפת חלק בלי לשנות את השאר, ומתי לא להשתמש בירושה.

**בסוף הדף תדעו:**
- לזהות Class שעושה יותר מדי ולפרק אותה
- לבנות Class שמורכבת מאובייקטים אחרים (has-a)
- להחליף רכיב באובייקט בלי לשנות את הקוד שמשתמש בו
- להבחין בין "הוא סוג של" (ירושה) ל"יש לו" (הרכבה)

### תרגיל 1: Class שעושה הכול

**מה עושים:** תכנון בלבד — מזהים Class שעושה יותר מדי, ומציעים חלוקה.

**הקוד:**

```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): ...
```

**המשימה:**
1. כמה אחריויות שונות יש כאן? כתבו כל אחת במילים.
2. הציעו חלוקה לכמה Classes: שם לכל Class, ואילו מתודות עוברות אליה.

**מה מגישים:** רשימת האחריויות, וטבלה של Class ומתודות.

{{< solution >}}
1. ארבע אחריויות: **ניהול העגלה** (פריטים וסכום), **חישוב מס**, **עיצוב קבלה**, **שליחת אימייל**. כל אחת היא "סיבה להשתנות" אחרת — זו **God Class**.
2. חלוקה:

| Class | אחריות | Methods |
|---|---|---|
| `Cart` | הפריטים | `add_item`, `subtotal` |
| `VatCalculator` | המס | `add_vat` |
| `ReceiptFormatter` | העיצוב | `format_receipt` |
| `EmailSender` | השליחה | `send_email` |
{{< /solution >}}

### תרגיל 2: Composition — העגלה והקופה

**מה בונים:** עגלת קניות וקופה — כשהקופה מורכבת מעגלה וממחשבון מס.

**מה לכתוב:**
1. `Cart` — עם `add` שמקבלת שם, מחיר וכמות (ברירת מחדל 1), ו-`subtotal` שמחזירה את סכום העגלה.
2. `IsraeliVat` — עם `apply` שמקבלת סכום ומחזירה אותו כולל 17% מע"מ.
3. `Checkout` — מקבלת עגלה ומחשבון מס ב-`__init__` ומחזיקה אותם (has-a).
4. ב-`Checkout`: `total` — הסכום של העגלה, אחרי מחשבון המס.

**קוד לבדיקה:**

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

**הפלט הרצוי:**

```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` לא מחשבת בעצמה — היא **מאצילה** (Delegation) לעגלה ולמחשבון המס, שכל אחד מהם מומחה בדבר אחד.
{{< /solution >}}

### תרגיל 3: מחליפים חלק

**מה בונים:** ממשיכים את הקופה — מחליפים את מחשבון המס בלי לשנות את `Checkout`.

**מה לכתוב:**
1. Class חדשה בשם `TaxExempt`, עם `apply` שמחזירה את הסכום בלי שינוי.
2. יוצרים קופה אחת עם `IsraeliVat` וקופה שנייה עם `TaxExempt`, על אותה עגלה, ומדפיסים את הסכום של שתיהן.
3. בדקו: לא שיניתם אף שורה ב-`Checkout`.

**הפלט הרצוי:**

```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
```

זה הכוח של **Composition + Polymorphism**: כל מחשבון מס מממש `apply`, והקופה עובדת עם כל אחד מהם.
{{< /solution >}}

### תרגיל 4: להוריש כדי לחסוך קוד?

**מה עושים:** מזהים ירושה לא נכונה, ומתקנים אותה עם הרכבה.

**הקוד:**

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

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

**המשימה:**
1. למה הירושה כאן לא נכונה, למרות שהקוד עובד? השתמשו במבחן "הוא סוג של".
2. שכתבו: `Engine` מקבלת כוח סוס, ו-`Car` מקבלת דגם ומנוע ומחזיקה אותו (has-a). ל-`Car` יש `start` שמשתמשת במנוע.

**קוד לבדיקה:**

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

**הפלט הרצוי:**

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

{{< solution >}}
1. מכונית **היא לא סוג של** מנוע — למכונית **יש** מנוע. ירשו רק כדי "לקבל" את `start`, ועכשיו `Car` כוללת כל מה שיש למנוע, ואי אפשר להחליף מנוע.
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 >}}

### תרגיל 5: מחשב מחלקים

**מה בונים:** מחשב שמורכב ממעבד, זיכרון ודיסק.

**מה לכתוב:**
1. `CPU` — עם מספר ליבות ומהירות ב-GHz.
2. `RAM` — עם גודל ב-GB.
3. `Disk` — עם גודל ב-GB.
4. `Computer` — מקבלת את שלושת החלקים ומחזיקה אותם. יש לה `__str__` שמתאר את המחשב כמו בפלט, ו-`can_run` שמקבלת מינימום ליבות ומינימום זיכרון ומחזירה `True` או `False`.

**קוד לבדיקה:**

```python
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
```

{{< 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
```

יש מספיק ליבות (8 ≥ 6), אבל רק 8GB RAM מתוך 16 הנדרשים. כל חלק יודע להציג את עצמו, ו-`Computer` רק מחבר.
{{< /solution >}}

<!-- exercises:end -->

## סיכום

- **SRP ב-Class**: סיבה אחת להשתנות. God Class (`...Manager`, `...System`) ‏←‏ מפרקים.
- **Composition** = אובייקט שמורכב מאובייקטים אחרים (**has-a**), ו**מאציל** להם עבודה.
- **Inheritance** רק ל-**is-a** אמיתי. ל-has-a או "uses-a" → Composition.
- **Favor Composition over Inheritance**: קשר רופף, וחלקים שאפשר להחליף.
- העבירו חלקים **כפרמטרים** ל-`__init__`, כדי שאפשר יהיה להחליף אותם (Composition + Polymorphism).
- מדברים עם חלק **דרך הממשק שלו**, לא דרך הפנים שלו.

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

## דוקומנטציה רשמית

- [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 -->
