# תרגול מודרך: מדרישה מילולית למבנה מחלקות<no value>

## מה נלמד?

זה שיעור **סדנה**. ניקח דרישה מילולית אחת, ונעבור **יחד** על כל התהליך:

1. קריאה וסימון.
2. החלטה על Classes.
3. אחריות ונתונים לכל Class.
4. קשרים בין ה-Classes.
5. שלד קוד.
6. מימוש.
7. בדיקה, ותיקון התכנון.

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

בפרויקט ה-OOP שתבנו בהמשך השבוע, **לפני כתיבת הקוד צריך שלב Design**. השיעור הזה הוא החזרה הגנרלית: אותו תהליך בדיוק, על בעיה אחרת, כשהמרצה מוביל.

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

### הדרישה

> **מערכת לחדר כושר "KodFit"**
>
> לחדר הכושר יש **מנויים**. לכל מנוי יש **שם**, **מספר מנוי** ו**סוג מנוי**: **Basic** (עד 8 כניסות בחודש) או **Premium** (ללא הגבלה). Premium עולה 250 ש"ח לחודש ו-Basic 150.
>
> בחדר הכושר יש **שיעורים** (יוגה, ספינינג...). לכל שיעור יש **שם**, **מדריך**, **יום ושעה**, ו**מספר מקומות מוגבל**.
>
> מנוי יכול **להירשם** לשיעור אם יש מקום, ואם לא עבר את מכסת הכניסות החודשית שלו. הרשמה נחשבת כניסה. מנוי יכול **לבטל** הרשמה, והכניסה חוזרת אליו.
>
> המערכת צריכה להציג: את כל השיעורים עם המקומות הפנויים, את השיעורים של מנוי מסוים, ואת ההכנסה החודשית מכל המנויים.

## תחביר

אין תחביר חדש. נשתמש בכל מה שלמדנו: Classes, `__init__`, properties, Encapsulation, Inheritance + Polymorphism, Composition ו-Exceptions משלנו.

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

### שלב 1 — קריאה וסימון

| שמות עצם | פעלים | כללים ומספרים |
|---|---|---|
| חדר כושר, מנוי, שם, מספר מנוי, סוג מנוי, Basic, Premium, שיעור, מדריך, יום ושעה, מקומות, הרשמה, כניסה, הכנסה | להירשם, לבטל, להציג | Basic: עד 8 כניסות, 150 ש"ח. Premium: ללא הגבלה, 250 ש"ח. מקומות מוגבלים. הרשמה = כניסה. ביטול מחזיר כניסה |

### שלב 2 — מה הן ה-Classes?

| מועמד | החלטה | נימוק |
|---|---|---|
| **מנוי** | ✓ Class `Member` | זהות, נתונים, כללים (מכסה). |
| **סוג מנוי** | ✓ **היררכיה**: `Plan` → `BasicPlan`, `PremiumPlan` | לכל סוג **התנהגות שונה** (מחיר, מגבלה), וזה בדיוק Polymorphism. אם יתווסף "Student" בעתיד, מוסיפים Class. |
| **שיעור** | ✓ Class `GymClass` | זהות, נתונים, כלל (מקומות). (`Class` הוא שם בעייתי, ולכן `GymClass`.) |
| **הרשמה** | ✗ לא Class נפרדת (בינתיים) | אין לה נתונים משלה מעבר ל"מי ואיפה". מספיק שהשיעור יחזיק את המנויים שלו. |
| **חדר כושר** | ✓ Class `Gym` | מחזיקה את כל המנויים והשיעורים, ומתאמת ביניהם. |
| מדריך, יום ושעה, שם | Attributes | ערכים, בלי התנהגות. |
| הכנסה | Method | מחושבת מסך המחירים. |

### שלב 3 — אחריות ונתונים

| Class | אחריות (משפט אחד) | נתונים | ממשק |
|---|---|---|---|
| `Plan` | מגדיר מחיר ומגבלת כניסות לסוג מנוי | — | `monthly_price()`, `visit_limit()` |
| `Member` | מנהל את המכסה החודשית של מנוי אחד | שם, מספר, plan, כניסות שנוצלו | `can_visit()`, `use_visit()`, `return_visit()` |
| `GymClass` | מנהל את רשימת הנרשמים לשיעור אחד | שם, מדריך, זמן, קיבולת, נרשמים | `has_room()`, `add()`, `remove()`, `is_registered()` |
| `Gym` | מתאם הרשמות ומספק דוחות | מנויים, שיעורים | `add_member()`, `add_class()`, `register()`, `cancel()`, `schedule()`, `classes_of()`, `monthly_revenue()` |

**החלטה חשובה:** איפה נבדק "האם מותר להירשם"? יש **שני** כללים: מקום בשיעור (שייך ל-`GymClass`) ומכסה (שייכת ל-`Member`). `Gym.register` **מתאמת**: שואלת את שניהם, ורק אם שניהם מסכימים, מבצעת את השינויים בשניהם.

### שלב 4 — קשרים

```text
            Gym
           /    \
   has many      has many
      /              \
  Member  ◄────────  GymClass
     │   (class holds its registered members)
   has-a
     │
    Plan  ◄── is-a ── BasicPlan
          ◄── is-a ── PremiumPlan
```

- `Gym` **has many** `Member` ו-`GymClass` → Composition (dicts פנימיים).
- `Member` **has a** `Plan` → Composition. (מנוי **הוא לא** סוג של תוכנית!)
- `BasicPlan` **is a** `Plan` → Inheritance + Polymorphism.
- `GymClass` מחזיקה **הפניות** ל-`Member`, ולא העתקים.

### שלב 5 — שלד

```python
class Plan:
    def monthly_price(self): ...
    def visit_limit(self): ...

class BasicPlan(Plan): ...
class PremiumPlan(Plan): ...

class Member:
    def __init__(self, member_id, name, plan): ...
    def can_visit(self): ...
    def use_visit(self): ...
    def return_visit(self): ...

class GymClass:
    def __init__(self, class_id, name, coach, time, capacity): ...
    def has_room(self): ...
    def add(self, member): ...
    def remove(self, member): ...

class Gym:
    def register(self, member_id, class_id): ...
    def cancel(self, member_id, class_id): ...
    def schedule(self): ...
    def monthly_revenue(self): ...
```

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

רגע לפני המימוש — **בדיקת התכנון מול הדרישה**, שורה אחר שורה:

| דרישה | מי מטפל |
|---|---|
| "Basic עד 8 כניסות, Premium ללא הגבלה" | `BasicPlan.visit_limit()` → 8. `PremiumPlan.visit_limit()` → `None` (ללא הגבלה). |
| "מחיר לפי סוג" | `monthly_price()` פולימורפית. |
| "להירשם אם יש מקום ואם לא עבר מכסה" | `Gym.register` → `GymClass.has_room()` + `Member.can_visit()`. |
| "הרשמה = כניסה" | `register` קורא ל-`member.use_visit()`. |
| "ביטול מחזיר כניסה" | `cancel` → `class.remove()` + `member.return_visit()`. |
| "להציג שיעורים עם מקומות פנויים" | `Gym.schedule()`. |
| "שיעורים של מנוי" | `Gym.classes_of(member_id)`. |
| "הכנסה חודשית" | `Gym.monthly_revenue()` ‏←‏ סכום `member.plan.monthly_price()`. |

כל שורה בדרישה "נוחתת" ב-Class ובמתודה. **אין שורה בלי בית, ואין Class בלי סיבה.**

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

### שלב 6 — מימוש. `week-3/gym.py`:

```python
class GymError(Exception):
    pass


class Plan:
    def monthly_price(self):
        raise NotImplementedError

    def visit_limit(self):
        raise NotImplementedError

    def name(self):
        return type(self).__name__.replace("Plan", "")


class BasicPlan(Plan):
    def monthly_price(self):
        return 150

    def visit_limit(self):
        return 8


class PremiumPlan(Plan):
    def monthly_price(self):
        return 250

    def visit_limit(self):
        return None


class Member:
    def __init__(self, member_id, name, plan):
        self.member_id = member_id
        self.name = name
        self.plan = plan
        self._visits_used = 0

    @property
    def visits_used(self):
        return self._visits_used

    def can_visit(self):
        limit = self.plan.visit_limit()
        return limit is None or self._visits_used < limit

    def use_visit(self):
        if not self.can_visit():
            raise GymError(self.name + " reached the monthly limit of " + str(self.plan.visit_limit()))
        self._visits_used = self._visits_used + 1

    def return_visit(self):
        if self._visits_used > 0:
            self._visits_used = self._visits_used - 1


class GymClass:
    def __init__(self, class_id, name, coach, time, capacity):
        if capacity <= 0:
            raise ValueError("capacity must be positive")
        self.class_id = class_id
        self.name = name
        self.coach = coach
        self.time = time
        self.capacity = capacity
        self._members = []

    @property
    def free_spots(self):
        return self.capacity - len(self._members)

    def has_room(self):
        return self.free_spots > 0

    def is_registered(self, member):
        return member in self._members

    def add(self, member):
        if self.is_registered(member):
            raise GymError(member.name + " is already in " + self.name)
        if not self.has_room():
            raise GymError(self.name + " is full")
        self._members.append(member)

    def remove(self, member):
        if not self.is_registered(member):
            raise GymError(member.name + " is not registered to " + self.name)
        self._members.remove(member)


class Gym:
    def __init__(self, name):
        self.name = name
        self._members = {}
        self._classes = {}

    def add_member(self, member):
        self._members[member.member_id] = member

    def add_class(self, gym_class):
        self._classes[gym_class.class_id] = gym_class

    def _member(self, member_id):
        if member_id not in self._members:
            raise GymError("no member " + str(member_id))
        return self._members[member_id]

    def _class(self, class_id):
        if class_id not in self._classes:
            raise GymError("no class " + str(class_id))
        return self._classes[class_id]

    def register(self, member_id, class_id):
        member = self._member(member_id)
        gym_class = self._class(class_id)
        if not member.can_visit():
            raise GymError(member.name + " reached the monthly limit")
        gym_class.add(member)
        member.use_visit()

    def cancel(self, member_id, class_id):
        member = self._member(member_id)
        self._class(class_id).remove(member)
        member.return_visit()

    def schedule(self):
        lines = []
        for gym_class in self._classes.values():
            lines.append(gym_class.time + " " + gym_class.name + " with " + gym_class.coach
                         + " - " + str(gym_class.free_spots) + " free")
        return lines

    def classes_of(self, member_id):
        member = self._member(member_id)
        return [c.name for c in self._classes.values() if c.is_registered(member)]

    def monthly_revenue(self):
        return sum(member.plan.monthly_price() for member in self._members.values())
```

### שלב 7 — בדיקה. בסוף הקובץ:

```python
def main():
    gym = Gym("KodFit")
    gym.add_member(Member(1, "Dana", PremiumPlan()))
    gym.add_member(Member(2, "Avi", BasicPlan()))
    gym.add_class(GymClass("Y1", "Yoga", "Noa", "Sun 08:00", 2))
    gym.add_class(GymClass("S1", "Spinning", "Tom", "Mon 19:00", 10))

    gym.register(1, "Y1")
    gym.register(2, "Y1")
    gym.register(1, "S1")

    attempts = [(1, "Y1"), (2, "Y9"), (9, "S1")]
    for member_id, class_id in attempts:
        try:
            gym.register(member_id, class_id)
        except GymError as error:
            print("Rejected:", error)

    gym.cancel(2, "Y1")
    for line in gym.schedule():
        print(line)
    print("Dana's classes:", gym.classes_of(1))
    print("Monthly revenue:", gym.monthly_revenue())


if __name__ == "__main__":
    main()
```

**פלט:**

```text
Rejected: Dana is already in Yoga
Rejected: no class Y9
Rejected: no member 9
Sun 08:00 Yoga with Noa - 1 free
Mon 19:00 Spinning with Tom - 9 free
Dana's classes: ['Yoga', 'Spinning']
Monthly revenue: 400
```

### מה הבדיקה גילתה? — תיקון תכנון

נבדוק את כלל המכסה עם מנוי Basic:

```python
gym2 = Gym("Test")
avi = Member(2, "Avi", BasicPlan())
gym2.add_member(avi)
for i in range(9):
    gym2.add_class(GymClass("C" + str(i), "Class " + str(i), "Coach", "Tue", 5))
for i in range(9):
    try:
        gym2.register(2, "C" + str(i))
    except GymError as error:
        print("Rejected:", error)
print("Avi used", avi.visits_used, "visits")
```

```text
Rejected: Avi reached the monthly limit
Avi used 8 visits
```

הכלל עובד. **אבל** שימו לב לחור: **מה אם `gym_class.add()` הצליח, ו-`member.use_visit()` נכשל?** אז המנוי רשום לשיעור, אבל בלי שנוצלה כניסה. במימוש שלנו זה לא קורה, כי בדקנו `can_visit()` **לפני** `add`. אבל זה מראה כלל חשוב: **כשמתודה משנה שני אובייקטים, בודקים את כל התנאים לפני שמשנים משהו.** בדיוק כמו ש-`transfer_to` בבנק קוראת ל-`withdraw` (שעלולה להיכשל) **לפני** `deposit`.

## טעות נפוצה

**הטעות 1: לדלג על שלבים 1–4 "כי ברור לי".** אחרי 200 שורות מגלים ש"הרשמה" הייתה צריכה להיות במקום אחר, וש-`Member` ו-`GymClass` "מכירים" זה את זה בצורה שגורמת לעדכונים כפולים. 15 דקות תכנון חוסכות שעות.

**הטעות 2: ירושה במקום Composition** — `class PremiumMember(Member)` נראה טבעי, אבל מה קורה כשמנוי **משדרג** מ-Basic ל-Premium? אי אפשר "להחליף Class" לאובייקט קיים. עם Composition (`member.plan = PremiumPlan()`) זו שורה אחת. **מה שעשוי להשתנות במהלך חיי האובייקט ‏←‏ Composition.**

**הטעות 3: כלל שנבדק במקום הלא נכון.** אם `GymClass.add` הייתה בודקת גם את המכסה של המנוי, השיעור היה צריך "להכיר" את חוקי התוכניות. כל Class בודקת **את הכללים שלה**, ו-`Gym` מתאמת.

**הטעות 4: לא לבדוק מקרי שגיאה.** הבדיקה כללה מזהים לא קיימים, הרשמה כפולה וחריגה ממכסה. בלעדיהם, היינו יודעים רק שהמסלול השמח עובד.

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

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

**מה מתרגלים בדף הזה:** כל הדרך מדרישה מילולית לקוד — בדוגמה של שירות השכרת קורקינטים. כל תרגיל הוא שלב אחד בתהליך: קודם תכנון, אחר כך Class אחרי Class.

**בסוף הדף תדעו:**
- לפרק דרישה ל-Classes, כללים וקשרים לפני שכותבים קוד
- לבנות היררכיה קטנה: Parent ו-Child שמשנה רק תעריף
- לשים כל כלל ב-Class שמחזיקה את הנתונים שלו
- לחבר כמה Classes למערכת אחת שעובדת

### תרגיל 1: מדרישה לרשימת מחלקות

**מה עושים:** שלב התכנון — בלי קוד.

**הדרישה:**

> "מערכת השכרת קורקינטים: **משתמש** שוכר **קורקינט** ל**נסיעה**. לכל קורקינט מספר וסוללה באחוזים. אסור לשכור קורקינט עם פחות מ-20% סוללה, או קורקינט שכבר בשימוש. נסיעה עולה 5 ש"ח לפתיחה ועוד 1 ש"ח לדקה. **קורקינט מהיר** עולה 2 ש"ח לדקה."

**המשימה:**
1. אילו Classes יש כאן? לכל אחת — מה היא יודעת (Attributes).
2. אילו כללים יש, ובאיזו Class כל כלל צריך לשבת?
3. מה הקשרים ביניהן: מי מחזיק את מי (has-a), ומי סוג של מי (is-a)?

**מה מגישים:** רשימת Classes עם Attributes, רשימת כללים, ורשימת קשרים.

{{< solution >}}
1. **Classes:** `User`, `Scooter`, `FastScooter`, `Ride`, ו-`RentalService` שמנהל את כולם.
2. **כללים:**
   - אין השכרה מתחת ל-20% סוללה.
   - אין השכרה של קורקינט שבשימוש.
   - מחיר נסיעה: 5 + דקות × תעריף לדקה (1 רגיל, 2 מהיר).
3. **קשרים:**
   - `FastScooter` **הוא סוג של** `Scooter` (is-a) — רק התעריף שונה.
   - `Ride` **מחזיקה** משתמש וקורקינט (has-a).
   - `RentalService` **מחזיק** הרבה קורקינטים ומשתמשים.
{{< /solution >}}

### תרגיל 2: Scooter ו-FastScooter

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

**מה לכתוב:**
1. Class בשם `Scooter`, עם מספר, אחוז סוללה, ו-`in_use` שמתחיל ב-`False`.
2. ב-`Scooter`: Class Attribute בשם `PER_MINUTE` שערכו 1, ו-`price` שמקבלת דקות ומחזירה 5 ועוד תעריף לדקה כפול הדקות.
3. Class בשם `FastScooter` שיורשת מ-`Scooter`, ומשנה רק את `PER_MINUTE` ל-2.

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

```python
print(Scooter(1, 80).price(10))
print(FastScooter(2, 90).price(10))
```

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

```text
15
25
```

{{< solution >}}
```python
UNLOCK_FEE = 5


class Scooter:
    PER_MINUTE = 1

    def __init__(self, number, battery):
        self.number = number
        self.battery = battery
        self.in_use = False

    def price(self, minutes):
        return UNLOCK_FEE + minutes * self.PER_MINUTE


class FastScooter(Scooter):
    PER_MINUTE = 2


print(Scooter(1, 80).price(10))
print(FastScooter(2, 90).price(10))
```

```text
15
25
```

`self.PER_MINUTE` קורא את הערך של ה-Class **של האובייקט** — ולכן `FastScooter` משתמש ב-2 בלי לדרוס את `price` בכלל.
{{< /solution >}}

### תרגיל 3: הכללים במקום אחד

**מה בונים:** ממשיכים את `Scooter` — שני הכללים של ההשכרה, במקום אחד.

**מה לכתוב:**
1. ב-`Scooter` מוסיפים `unlock`.
2. אם הסוללה מתחת ל-20%: `ValueError` עם ההודעה הראשונה שבפלט.
3. אם הקורקינט כבר בשימוש: `ValueError` עם ההודעה השנייה שבפלט.
4. אחרת: מסמנים שהקורקינט בשימוש.

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

```python
s = Scooter(1, 15)
try:
    s.unlock()
except ValueError as error:
    print("error:", error)
s.battery = 60
s.unlock()
print(s.in_use)
try:
    s.unlock()
except ValueError as error:
    print("error:", error)
```

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

```text
error: battery too low: 15%
True
error: scooter 1 is in use
```

{{< solution >}}
```python
UNLOCK_FEE = 5
MIN_BATTERY = 20


class Scooter:
    PER_MINUTE = 1

    def __init__(self, number, battery):
        self.number = number
        self.battery = battery
        self.in_use = False

    def price(self, minutes):
        return UNLOCK_FEE + minutes * self.PER_MINUTE

    def unlock(self):
        if self.battery < MIN_BATTERY:
            raise ValueError("battery too low: " + str(self.battery) + "%")
        if self.in_use:
            raise ValueError("scooter " + str(self.number) + " is in use")
        self.in_use = True


s = Scooter(1, 15)
try:
    s.unlock()
except ValueError as error:
    print("error:", error)
s.battery = 60
s.unlock()
print(s.in_use)
try:
    s.unlock()
except ValueError as error:
    print("error:", error)
```

```text
error: battery too low: 15%
True
error: scooter 1 is in use
```
{{< /solution >}}

### תרגיל 4: נסיעה

**מה בונים:** נסיעה — שמחברת משתמש לקורקינט.

**מה לכתוב:**
1. Class בשם `Ride`, שמקבלת שם של משתמש וקורקינט ומחזיקה אותם.
2. ב-`__init__` של `Ride`: קוראים ל-`unlock` של הקורקינט — הנסיעה נפתחת רק אם מותר.
3. `finish` — מקבלת דקות, משחררת את הקורקינט ומחזירה את מחיר הנסיעה.

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

```python
scooter = FastScooter(7, 90)
ride = Ride("Dana", scooter)
print(scooter.in_use)
print(ride.finish(12))
print(scooter.in_use)
```

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

```text
True
29
False
```

{{< solution >}}
```python
UNLOCK_FEE = 5
MIN_BATTERY = 20


class Scooter:
    PER_MINUTE = 1

    def __init__(self, number, battery):
        self.number = number
        self.battery = battery
        self.in_use = False

    def price(self, minutes):
        return UNLOCK_FEE + minutes * self.PER_MINUTE

    def unlock(self):
        if self.battery < MIN_BATTERY:
            raise ValueError("battery too low: " + str(self.battery) + "%")
        if self.in_use:
            raise ValueError("scooter " + str(self.number) + " is in use")
        self.in_use = True


class FastScooter(Scooter):
    PER_MINUTE = 2


class Ride:
    def __init__(self, user, scooter):
        scooter.unlock()
        self.user = user
        self.scooter = scooter

    def finish(self, minutes):
        self.scooter.in_use = False
        return self.scooter.price(minutes)


scooter = FastScooter(7, 90)
ride = Ride("Dana", scooter)
print(scooter.in_use)
print(ride.finish(12))
print(scooter.in_use)
```

```text
True
29
False
```

`Ride` לא בודקת את הכללים בעצמה — היא קוראת ל-`unlock`, והכללים נשארים בתוך `Scooter`.
{{< /solution >}}

### תרגיל 5: שירות ההשכרה

**מה בונים:** השירות כולו — מחזיק את כל הקורקינטים ויודע אילו פנויים.

**מה לכתוב:**
1. Class בשם `RentalService`, שמחזיקה מילון של קורקינטים לפי מספר.
2. `add` — מוסיפה קורקינט למילון.
3. `get` — מקבלת מספר ומחזירה את הקורקינט.
4. `available` — מחזירה רשימה של מספרי הקורקינטים שאפשר לשכור עכשיו: סוללה של 20% לפחות, ולא בשימוש.

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

```python
service = RentalService()
service.add(Scooter(1, 80))
service.add(Scooter(2, 10))
service.add(FastScooter(3, 70))
Ride("Dana", service.get(3))
print(service.available())
```

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

```text
[1]
```

{{< solution >}}
```python
UNLOCK_FEE = 5
MIN_BATTERY = 20


class Scooter:
    PER_MINUTE = 1

    def __init__(self, number, battery):
        self.number = number
        self.battery = battery
        self.in_use = False

    def can_rent(self):
        return self.battery >= MIN_BATTERY and not self.in_use

    def unlock(self):
        if self.battery < MIN_BATTERY:
            raise ValueError("battery too low: " + str(self.battery) + "%")
        if self.in_use:
            raise ValueError("scooter " + str(self.number) + " is in use")
        self.in_use = True


class FastScooter(Scooter):
    PER_MINUTE = 2


class Ride:
    def __init__(self, user, scooter):
        scooter.unlock()
        self.user = user
        self.scooter = scooter


class RentalService:
    def __init__(self):
        self._scooters = {}

    def add(self, scooter):
        self._scooters[scooter.number] = scooter

    def get(self, number):
        return self._scooters[number]

    def available(self):
        result = []
        for number, scooter in self._scooters.items():
            if scooter.can_rent():
                result.append(number)
        return result


service = RentalService()
service.add(Scooter(1, 80))
service.add(Scooter(2, 10))
service.add(FastScooter(3, 70))
Ride("Dana", service.get(3))
print(service.available())
```

```text
[1]
```

קורקינט 2 — סוללה נמוכה. קורקינט 3 — בשימוש. הוספנו ל-`Scooter` את `can_rent()`, כדי שהשירות **ישאל** את הקורקינט ולא יבדוק את הנתונים שלו בעצמו.
{{< /solution >}}

<!-- exercises:end -->

## סיכום

- תהליך התכנון: **סימון ‏←‏ Classes ‏←‏ אחריות ונתונים ‏←‏ קשרים ‏←‏ שלד ‏←‏ מימוש ‏←‏ בדיקה**.
- **כל שורה בדרישה** צריכה "לנחות" ב-Class ובמתודה. **כל Class** צריכה סיבה.
- **התנהגות שונה לפי סוג** ‏←‏ היררכיה + Polymorphism (Plan). **משהו שעשוי להשתנות במהלך החיים** ‏←‏ Composition (member.plan).
- כל Class בודקת **את הכללים שלה**, ו-Class מתאמת (Gym) מחברת ביניהן.
- כשפעולה משנה כמה אובייקטים: **בודקים את כל התנאים לפני שמשנים משהו.**
- דרישה חדשה = עדכון התכנון. ותכנון טוב מאפשר להוסיף בלי לשבור.

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

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

- [A First Look at Classes](https://docs.python.org/3/tutorial/classes.html#a-first-look-at-classes) — מהגדרה למחלקה
- [Class and Instance Variables](https://docs.python.org/3/tutorial/classes.html#class-and-instance-variables) — אילו נתונים שייכים לאובייקט

<!-- docs:end -->
