מה זה?

מחלקת Invoice (חשבונית) שמחשבת סכום, שומרת לקובץ, וגם שולחת אימייל — נשמעת נוח, אבל כל שינוי בלוגיקת המייל מסכן לשבור את חישוב הסכום, כי הכל יושב יחד באותה מחלקה. SOLID הם חמישה עקרונות לתכנון מחלקות שקל לתחזק ולהרחיב — השיעור הזה מתחיל בשני הראשונים: SRP (Single Responsibility) אומר שלכל מחלקה צריכה להיות סיבה אחת בלבד להשתנות; OCP (Open/Closed) אומר שאפשר להרחיב התנהגות בלי לשנות קוד קיים.

מילות מפתח שחשוב לזכור

• SRP (Single Responsibility Principle) — לכל מחלקה סיבה אחת ויחידה להשתנות; “אחריות” אחת ברורה

• OCP (Open/Closed Principle) — פתוח להרחבה (אפשר להוסיף התנהגות), סגור לשינוי (לא נוגעים בקוד קיים שכבר עובד)

• “סיבה להשתנות” — אם שינוי בדרישה עסקית אחת (למשל “שנו את פורמט המייל”) מחייב לגעת בקוד שאחראי גם על משהו אחר (חישוב סכום) — זו הפרת SRP

// Before SRP — one class with several mixed responsibilities
class Invoice
{
    public double CalculateTotal(List<double> items) => items.Sum();
    public void SaveToFile(string content) { /* write to file */ }
    public void SendEmail(string to, string content) { /* send email */ }
}
// After SRP — each class is responsible for exactly one thing
class InvoiceCalculator
{
    public double CalculateTotal(List<double> items) => items.Sum();
}

class InvoiceFileWriter
{
    public void SaveToFile(string content) { /* write to file */ }
}

class InvoiceEmailSender
{
    public void SendEmail(string to, string content) { /* send email */ }
}
// A change to the email format only touches InvoiceEmailSender — the rest of the code is unaffected
// OCP — adding a new discount type without touching existing code
abstract class DiscountStrategy
{
    public abstract double Apply(double price);
}

class NoDiscount : DiscountStrategy
{
    public override double Apply(double price) => price;
}

class PercentageDiscount : DiscountStrategy
{
    private double _percent;
    public PercentageDiscount(double percent) => _percent = percent;
    public override double Apply(double price) => price * (1 - _percent / 100);
}

// Adding a BlackFridayDiscount in the future = a new class, without touching existing code at all
double ApplyDiscount(double price, DiscountStrategy strategy) => strategy.Apply(price);

הסבר עיקרי

SRP לא אומר “מחלקה אחת = מתודה אחת” — הוא אומר “מחלקה אחת = אחריות אחת”. InvoiceCalculator יכולה להכיל כמה מתודות עזר לחישוב (מע"מ, הנחות, עיגול) — כולן עדיין תחת אחריות אחת (“חישוב”). הבעיה היא כשמחלקה אחת מכילה כמה אחריות שונות לגמרי (חישוב + כתיבה לקובץ + שליחת מייל) — כל אחת מהן “סיבה נפרדת להשתנות”.

OCP נבנה ישירות על מה שכבר תרגלנו — abstract class DiscountStrategy עם override לכל סוג הנחה זו בדיוק ההפשטה והפולימורפיזם מהיחידה הקודמת! ההבדל הוא רק המסגור: OCP אומר שהמטרה של הפשטה כאן היא ספציפית — לאפשר הוספת BlackFridayDiscount חדש בעתיד כמחלקה נוספת, בלי לפתוח ולערוך את ApplyDiscount או את NoDiscount/PercentageDiscount הקיימות בכלל.

הקשר בין SRP ל-OCP — הם משלימים זה את זה: SRP מוודא שכל מחלקה ממוקדת מספיק כדי שלהרחיב אותה (OCP) יהיה קל ובטוח. מחלקה שכבר עמוסה בכמה אחריות (הפרת SRP) קשה הרבה יותר להרחיב בלי לשבור משהו — כי כל שינוי עלול לפגוע באחריות אחרת שיושבת באותו מקום.

יתרונות

SRP הופך כל מחלקה לקלה יותר להבנה, בדיקה (Unit Testing) ותחזוקה בנפרד; OCP מאפשר להוסיף פיצ’רים חדשים (סוג הנחה, סוג תשלום) בלי לסכן קוד קיים שכבר נבדק ועובד בפרודקשן.

חסרונות

פירוק יתר לפי SRP (מחלקה לכל פעולה זעירה) עלול ליצור פיזור-יתר וקשה למעקב; OCP דורש לחזות מראש איפה תרחבו בעתיד — הפשטה שגויה (במקום הלא נכון) מוסיפה מורכבות בלי תועלת אמיתית.

נקודות חשובות

• SRP = סיבה אחת להשתנות לכל מחלקה, לא “מתודה אחת בלבד”

• OCP = פתוח להרחבה (מחלקות חדשות), סגור לשינוי (קוד קיים לא נוגעים בו)

• OCP ממומש בפועל דרך abstract class/Interface + פולימורפיזם — כלים שכבר מוכרים

• הפרת SRP מקשה על יישום OCP — מחלקה עמוסה קשה יותר להרחיב בבטחה

טעויות נפוצות

• לפרש SRP כ"מחלקה אחת = מתודה אחת בדיוק" — זו טעות; המדד הוא אחריות, לא כמות מתודות

• להוסיף התנהגות חדשה על ידי if/switch נוסף בקוד קיים במקום מחלקה חדשה — מפר OCP

• לפרק יותר מדי לפי SRP בלי צורך אמיתי, ולהפוך קוד פשוט למפוזר מדי

סיכום

SRP אומר שלכל מחלקה צריכה להיות סיבה אחת ויחידה להשתנות — לא לערבב חישוב, שמירה ושליחת מייל תחת אותה מחלקה. OCP אומר שאפשר להרחיב התנהגות (למשל סוג הנחה חדש) על ידי הוספת מחלקה חדשה שמממשת חוזה קיים (abstract class/Interface), בלי לגעת בקוד קיים שכבר עובד. שני העקרונות משלימים זה את זה — SRP מייצר מחלקות ממוקדות שקל וקל להרחיב לפי OCP.

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

Microsoft Learn — SOLID principles


תרגילים

תרגיל 1 — פירוק לפי SRP

המשימה: קחו מחלקה UserManager היפותטית שמכילה ValidateUser, SaveToDatabase ו-SendWelcomeEmail יחד, ופרקו אותה לשלוש מחלקות נפרדות, כל אחת עם אחריות אחת.

בדיקה: כל מחלקה חדשה עוסקת באחריות אחת בלבד, וקוד קורא שמרכיב אותן יחד (למשל ב-Main) עדיין מבצע את אותה זרימה מקורית בהצלחה.

תרגיל 2 — הרחבה לפי OCP

המשימה: הוסיפו class FixedAmountDiscount : DiscountStrategy (הנחה בסכום קבוע, לא אחוז) לדוגמה מהשיעור, בלי לשנות אף שורה בקוד הקיים.

בדיקה: ApplyDiscount הקיימת (שלא שונתה) עובדת נכון גם עם אסטרטגיית ההנחה החדשה.


פרויקט מסכם

המשימה: תכננו מחדש מערכת “עיבוד הזמנות” לפי SRP ו-OCP.

דרישות:

  1. פרקו לפי SRP: OrderCalculator (חישוב סכום), OrderValidator (בדיקת תקינות), OrderNotifier (התראה) — כל אחת מחלקה נפרדת עם אחריות אחת
  2. יישמו OCP על סוג ההתראה: abstract class OrderNotifier עם לפחות שני מימושים (EmailNotifier, ConsoleNotifier)
  3. ב-Main, הרכיבו את כל המחלקות יחד לזרימת עיבוד הזמנה מלאה אחת

בדיקה: הוספת סוג התראה שלישי (למשל SmsNotifier) לא דורשת שינוי באף אחת מהמחלקות הקיימות — רק מחלקה חדשה.

מה בפרק הבא

בפרק הבא — SOLID: LSP, ISP ו-DIP: שלושת העקרונות הנותרים, שמשלימים את התמונה של תכנון מחלקות גמיש ובטוח לשינוי.