עקרונות ה-SOLID לפיתוח קוד יציב ודאי מוכרים לחלקכם. מה שלא בהכרח לימדו אתכם, הוא איך להפוך את העקרונות האלו למתודולוגית עבודה שהיא הרבה יותר פשוטה ואינטואיטיבית מאשר לזכור שמות של עקרונות.
לאלו שהמושג אינו מוכר להם, תגיע מיד תזכורת קצרה.
לאלו שהמושג מוכר להם, אבל הם מנסים להיזכר ממש עכשיו מה, לעזאזל, אומרת אחת האותיות ב-SOLID שבדיוק פרחה מזכרונם – הפוסט הזה יפתור את הבעיה.
ואם אתם מאלו שגם מכירים את המונח וגם מיישמים אותו ביומיום על כל חמשת חלקיו – אני מקווה שאתן עזרה קטנה באיך להסביר את העקרונות לג'וניורים שאתם חונכים בצוות.
חמשת העקרונות של הדוד בוב
חמשת העקרונות נטבעו על ידי רוברט ס. מרטין, ("הדוד בוב"), מהוגי ה-Agile Programming, בתחילת שנות האלפיים, ובזכותו של מייקל פת'רס קיבלו את השם הקליט SOLID, שמייצג אותם – אות לכל עקרון. מרטין ייעץ לחברות גדולות, כמו זירוקס ואחרות, ונתקל ב"מחלות" שאינן תוצאה של קידוד לא נכון או של פרטים ספציפיים, אלא של גישה כללית לכתיבת קוד שלא ייפגע מאבולוציה טבעית של מערכת. מטרתם של העקרונות הוא להוות כלי שמאפשר דיזיין – וכתוצאה מכך גם קידוד – שיאפשר תחזוקה יעילה של התוכנה והקוד-בייס לאורך זמן. העקרונות הם גנריים, ומתאימים לכל מערכת.
חמשת העקרונות אמנם יוצרים שם קליט, אך כשלעצמם, הם קליטים פחות… אלו הם העקרונות:
SRP: Single Responsibility Principle – S (עקרון האחריות היחידה)
OCP: Open/Closed Principle – O (עקרון ה"פתוח/סגור")
LSP: Liskov Substitution Principle – L (עקרון ההחלפה של ליסקוב)
ISP: Interface Segregation Principle – I (עקרון הפרדת הממשקים)
DIP: Dependency Inversion Principle – D (עקרון היפוך התלות)
מה, בעצם, המשמעות של כל אחד מהם? אני אתן כאן הסבר קצר, ברמת העיקרון, מבלי להיכנס לפרטים.
עקרון האחריות היחידה
עקרון ה-Single Responsibility אומר שמחלקה אחת צריכה לטפל ב"בעיה" אחת. אם אנחנו מתכננים מחלקה שצריכה לבצע מספר פעולות – אנחנו מתכננים לא נכון. בהסתכלות המקורית – לכל מחלקה תיתכן "סיבה אחת" לשינוי בהמשך החיים של המערכת.
כך, למשל, אם אנחנו מתכננים מחלקה שמפרסרת קובץ XML המכיל מידע ומוציאה סיכום מסודר ומפורמט ב-HTML, אנחנו מטילים יותר מדי אחריות על אותה המחלקה. אם בעתיד ישתנה הפורמט של קובץ המידע – ניאלץ לשנות את המחלקה, ואנו עלולים לפגוע בקוד שאחראי לייצור ה-HTML, שכלל לא השתנה. באותה המידה, אם בעתיד ישתנה פורמט התצוגה – נצטרך לשנות את המחלקה, ואנו עלולים לפגוע בקוד (שלא לדבר על צורך בקומפילציה מחדש, בשפות רלוונטיות) שאחראי לקריאת המידע, והוא אגנוסטי לחלוטין לשינוי הנוכחי.
לכן, אם אנחנו עומדים בפני דיזיין ומגיעים למסקנה שמחלקה מסויימת מתוכננת לבצע יותר ממשימה אחת – אנחנו צריכים לפרק אותה למספר מחלקות שונות שיאפשרו את הפחתת התלויות והפרדת הקוד.
עקרון ה"פתוח/סגור"
זוהי בעצם אבחנה בין הדברים שאותם אנו בוחרים לקבע בקוד הנוכחי (והחשוב מכולם: האינטרפייס) לבין הצורך בכתיבת קוד שיאפשר הרחבה של היכולות הנוכחיות. ההגדרה המקורית אומרת כי ישויות התוכנה שנכתוב צריכות להיות "פתוחות להרחבה אך סגורות לשינויים". עם התפתחות מנגנוני התכנות במתודולוגיות OOP, קיבל העקרון הזה צורה קצת שונה מכפי שהתייחס אליו מרטין במקור, בזכות יכולות הפולימורפיזם. אפשר, לכן, להתייחס לזה כך: האינטרפייס כלפי מחלקות יורשות צריך להיות כזה שמגדיר בצורה ברורה אילו חלקים ניתנים להרחבה.
עקרון ההחלפה של ליסקוב
העקרון הזה הוא אחד הבסיסיים ביותר בעולם ה-OOP, ובכל זאת, גם מפתחים מנוסים חורגים ממנו לעתים מבלי משים. העקרון הוא פשוט: כל טיפוס (מחלקה) שיורש מטיפוס אחר, חייב להיות חליפי לו, כך שאם מחלקה B יורשת את מחלקה A, אז בכל מקום בקוד שבו מצפים לאובייקט מסוג A, נוכל לספק אובייקט מסוג B ללא כל שינוי בהתנהגות. הכלל הזה ידוע כדרישה לקיום יחסי "is-a" בין מחלקת אב למחלקה היורשת (אם B יורש את A, אז B הוא A).
הדרישה של ליסקוב היא מחמירה יותר מכפי שרוב המפתחים מבינים אותה, והיא דורשת – מלבד הימנעות משינוי התנהגות כמו בדוגמאות הקלאסיות של מלבן/ריבוע או אליפסה/עיגול, גם שתי דרישות חשובות נוספות:
זריקת Exceptions היא חלק מחתימת פונקציה, ולכן מימוש מחדש של פונקציה במחלקה יורשת לא יזרוק Exceptions שלא היו יכולות להיזרק מהפונקציה המתאימה במחלקה המקורית (אלא אם כן ה-Exceptions עצמם יורשים מ-Exceptions שנזרקו במחלקה המקורית; אפשר לראות דוגמא לעקרון הזה בפוסט שלי על ניהול Exceptions).
שמירה על "היסטורית שינויים" – אם מחלקה B יורשת ממחלקה A, אסור לה לגרום לשינויים במשתנים הפנימיים (data members) שירשה מ-A בדרך שלא ניתן לבצע על מחלקה A. במילים אחרות, אסור לגרום למצב שבו החלקים ה"A-יים" של אובייקט מסוג B נמצאים במצב שאליו לעולם לא יכול להגיע אובייקט מסוג A. משתנים חברים שקיימים ב-B אך לא קיימים ב-A ניתן כמובן לשנות בכל דרך.
עקרון הפרדת הממשקים
לכל "לקוח" (Client) של פיסת קוד יש צורך להכיר אך ורק את הפונקציונליות הנדרשת לו. נניח, למשל, רשומה בבסיס-נתונים כלשהו מכילה מידע על פרטי מוצר, כמות במלאי, יצרן וכדומה. מערכות ORM מאפשרות לנו לייצג בקלות כל רשומה כאובייקט מטיפוס רלוונטי. הפתרון הטריוויאלי יהיה לאפשר לכל לקוח לתשאל את האובייקט דרך הטיפוס הזה. כך, למשל, מחלקת רכש תוכל לפנות לאובייקט לצורך קבלת פרטי יצרן, בעוד אתר האינטרנט יאפשר פניה למידע על קיום המוצר במלאי. טריוויאלי, אבל לא מוצלח, כמו שהבין מרטין כשסייע לחברת זירוקס להיחלץ ממשבר תוכנה רציני.
במקרה כזה, יש צורך לייצר ממשקים שונים לכל לקוח. ממשק עבור אתר האינטרנט יאפשר, למשל, לקבל פרטים המעניינים את הלקוח, לציין האם יש במלאי מוצרים מסוג זה, ולבצע הזמנה. ממשק עבור מחלקות ניהול יאפשר קבלת פרטים על היצרן, וכן הלאה. העקרון הזה, שנכתב בדמן של מכונות הדפוס הרב-תכליתיות של זירוקס, מדגיש את הצד הפחות אינטואיטיבי: מוטב להחזיק מספר רב של ממשקים, ואפילו מאוד ספציפיים, מאשר להחזיק ממשק יחיד, גנרי וכל-יכול. מי שמכיר מעט Design Patterns יודע שישנם לא מעט patterns שעוסקים אך ורק ביצירת ממשקים. התכלית של כולם זהה: יש להפריד את התלות בין ישויות שאינן קשורות בהכרח זו לזו.
עקרון היפוך התלות
השם שלו מעט מבלבל, כיוון שבכל מערכת תוכנה אנחנו מעדיפים שלא תהיינה תלויות בכלל… אבל יש לזה סיבה. במערכת "טריוויאלית", גם אם יש חלוקה לשכבות אחריות שונות, ישנה תלות חזקה בין השכבות. התלות הזו תהיה בדרך כלל "מלמעלה למטה": אם מערכת מורכבת ממספר אפליקציות, היא צריכה להכיר אותן. אם כל אפליקציה משתמשת במספר רכיבי תוכנה, היא צריכה להכיר אותם. אם כל רכיב כזה משתמש במספר ספריות שירות, או בדרייברים מסויימים, הוא צריך להכיר אותם. במערכת כזו, למרות שייתכן שהקוד כתוב היטב והאחריות מוגדרת בצורה ברורה – יהיה קשה מאוד לבצע re-use של קוד, או להחליף רכיבים באיזורים ה"נמוכים" של המערכת, מבלי לפגוע בשכבות הגבוהות יותר.
עקרון "היפוך התלות" קובע כי תלות כזו היא בעייתית, ולכן יש לייצר הפרדת תלויות באמצעות ממשקים. כך, למשל, המערכת תכיר את הממשק לכל אפליקציה, אבל לא את האפליקציה עצמה; אם, מתישהו, יהיה צורך להחליף את האפליקציה באפליקציה אחרת, המספקת שירות דומה אולם ממומשת אחרת – ניתן יהיה לעשות זאת בקלות. באותה הצורה, האפליקציה לא תהיה תלויה ברכיביה, אלא רק תכיר את הממשק שלהם; בכל רגע ניתן יהיה להחליף את הרכיבים הללו באחרים, המספקים שירות דומה.
אם כך – למה "היפוך התלות"? אנחנו מנסים למנוע תלויות לחלוטין, לא?! הסיבה היא שמימוש פשוט ומקובל של הגישה הזו, כולל הגדרה של אינטרפייס (או מחלקה אבסטרקטית) בשכבה ה"גבוהה", שאותה מממשים הרכיבים בשכבות הנמוכות יותר. כך, למשל, האפליקציה תגדיר את הממשק שלו היא מצפה מכל רכיב שלה. בשכבת הרכיבים, כל מודול יממש את הממשק הזה. מה שנוצר כאן הוא "תלות הפוכה" – השכבה הנמוכה צריכה להכיר את השכבה הגבוהה כדי לקבל את פרטי הממשק. מימוש טוב יותר הוא כזה שמוציא לחלוטין את הגדרת הממשק מתלות בשכבה כלשהיא. כך, ניתן למשל להגדיר "ממשק רכיב", במודול עצמאי, שבו ישתמשו מודולי הרכיבים כדי לממש אותו, ואותו יכירו מודולי האפליקציה כדי לתפעל אותו.
מה, בעצם, היה לנו פה?
ראינו חמישה עקרונות חשובים מאוד, שעלינו להתחשב בהם בעת תכנון או כתיבה של כל מערכת תוכנה. אפשר לנסות לזכור אותם באמצעות ראשי התיבות SOLID, אבל אני מניח שגם מי שזוכר ש-L זה "ליסקוב", לא בהכרח ייזכר באותו הרגע מה משמעות העיקרון. אני מציע, לכן, להתייחס למשמעות האמיתית של העקרונות הללו, בדרך שגם מאפשרת להסתכל עליהם בצורה ברורה יותר וגם תאפשר לעבור עליהם בקלות בכל תכנון, קידוד או ריוויו.
לכל מחלקה ישנם חמישה אספקטים חשובים:
- הבסיסי ביותר: מה תפקידה?
- מה היא מגדירה עבור מחלקות יורשות עתידיות, אם בכלל?
- אילו התחייבויות יש לה כלפי המחלקות מהן ירשה, אם יש כאלו?
- איזה ממשק היא מספקת ל-clients שלה?
- בעזרת איזה ממשק היא פונה למחלקות שהיא ה-client שלהן?
מבחינה גרפית, ניתן להסתכל על זה כך:
באופן לא מפתיע, חמשת עקרונות ה-SOLID מתייחסים בדיוק לחמשת האספקטים הללו. אם "נניח" כל עקרון במקום המתאים לו בסכמה הזו, נקבל משהו כזה:
כעת, ההגיון במערכת חמשת החוקים הללו הרבה יותר ברור, ובעיקר: ניתן לזכור אותו באמצעות התייחסות לכל ממשקי המחלקה – "למעלה", למחלקת האב, "למטה" – למחלקות יורשות, "שמאלה" – הממשק עבור קליינטים חיצוניים, ו"ימינה" – הממשק עבור רכיבים פנימיים.
ממש מגניב, למרות שקראת לא פעם אחד בשפות אחרות
מאוד אהבתי את חלק "מה, בעצם, היה לנו פה?"
תודה!
שמח לשמוע.
למעשה, זה החלק שבשבילו בעיקר כתבתי את הפוסט – עד כמה שזה ברור כשמסתכלים על זה ככה, לא ראיתי בשום מקום התייחסות לחלוקת האחריות הזו בין חמשת העקרונות.
מלמד וכתוב בצורה ברורה מאד. תודה!
This is the right blog for anybody who would like to find out about this topic.
You understand so much its almost tough to argue with you (not that I really would want to…HaHa).
You certainly put a fresh spin on a topic that has been written about for decades.
Excellent stuff, just excellent!