SOLID — SRP ו-OCP
מה זה?
מחלקת 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.
דרישות:
- פרקו לפי SRP:
OrderCalculator(חישוב סכום),OrderValidator(בדיקת תקינות),OrderNotifier(התראה) — כל אחת מחלקה נפרדת עם אחריות אחת - יישמו OCP על סוג ההתראה:
abstract class OrderNotifierעם לפחות שני מימושים (EmailNotifier,ConsoleNotifier) - ב-
Main, הרכיבו את כל המחלקות יחד לזרימת עיבוד הזמנה מלאה אחת
בדיקה: הוספת סוג התראה שלישי (למשל SmsNotifier) לא דורשת שינוי באף אחת מהמחלקות הקיימות — רק מחלקה חדשה.
מה בפרק הבא
בפרק הבא — SOLID: LSP, ISP ו-DIP: שלושת העקרונות הנותרים, שמשלימים את התמונה של תכנון מחלקות גמיש ובטוח לשינוי.