MasterScripter

עקרון יישום פתוח-סגור

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

ברוכים הבאים והשבים לפוסט מספר 2 מתוך 5 בסדרת S.O.L.I.D / posts.
לאלה מכם שהצטרפו אלינו עכשיו – עצרו רגע! על עקרון אחריות יחידה קראתם?

כן-לא-שחור-לבן, אנחנו יוצאים לדרך:

עקרון יישום פתוח-סגור

באנגלית: Open-Closed Principle

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

מחזור החיים של מערכת, לרוב, אורך יותר מגרסה אחת.
אם להוציא את החריגים, מערכת נוצרת כדי לשרת לקוח במשך תקופה ארוכה, לעיתים אף עשרות שנים! (מישהו אמר צה"ל?)
אם נקודת המוצא הזו נכונה גם לך, אז אחת ממשימות-העל שלך היא לפתח אותה ככה שניתן יהיה לתחזק, לשנות ולהרחיב אותה גם בעוד 4, 7, 10 שנים.

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

מרכיבי תוכנה (מחלקות, מודולים, פונקציות וכו') צריכים להיות פתוחים להרחבה, אבל סגורים לעריכה

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

  1. פתוח להרחבה
    ניתן להוסיף לרכיב פונקציונליות ואף להתאים את ההתנהגות שלו כך שיתאים לדרישות התוכנה
  2. סגור לעריכה
    קוד-הבסיס של הרכיב בלתי ניתן לשינוי ולאף אחד אין הרשאה לבצע בו שינויים.

כדי להבין מה הכוונה –
נסתכל על שתי המחלקות הבאות, שמתארות עובדים בפרוייקט:

העקרון דיי פשוט: מחלקה שמתארת מפתח ומחלקה שמתארת מעצב.
לכל עובד יש פונקציית עבודה:

  • Programmer מתכנת באמצעות הפונקציה code
  • Designer מעצב באמצעות הפונקציה design

המחלקה ProjectManagement אחראית לתפעול העובדים בפרוייקט:

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

לפרוייקט שלנו הצטרף משווק שאחראי בין היתר על פרסום הפרויקט:

כדי לשלב אותו בתהליך העבודה, עלינו להוסיף אותו לפונקציה proccess במחלקה ProjectManagement:

הפרה! זוכרים מה אמרנו על עקרון יישום פתוח-סגור? קבלו תזכורת:

הוספת התנהגות חדשה תיעשה על-ידי הוספת קוד חדש ולא על ידי שינוי הקוד הקיים

הדוגמה גם ממחישה למה העקרון חשוב: התנהגות של מחלקה משתנה כל הזמן. אם בהמשך הפרוייקט יצטרפו גם עובדים אחרים (בודקי תוכנה, מהנדסים, מנהל כספים וכו'), נצטרך להמשיך ולהתאים את הפונקציה עד אין-סוף.
אז מה כן עושים?

פשטות היא התשובה

הדרך הנכונה היא ליצוק את המחלקות שמתארות עובדים לתוך תבנית, או במילים פחות פואטיות – לכתוב ממשק:

כל מחלקה של עובד תיישם את הממשק; כפועל יוצא – על מחלקה ליישם את הפונקציה Work.
ככה זה יראה:

ועכשיו לקסם: קבלו את המחלקה ProjectManagement, אחרי מייק-אובר:

מה קרה פה?

הפונקציה proccess מקבלת פרמטר מסוג Employee שאנחנו יודעים שבוודאות מיישם פונקציה בשם work.

ולצד צמצום הקוד, שהוא חשוב בפני עצמו, עקרון יישום פתוח-סגור מאפשר לנו להוסיף קוד ללא צורך בעריכת הקוד הקיים.
רוצים הוכחה? אין בעיה: לפרוייקט שלנו הצטרף מנהל כספים ועלינו להוסיף אותו לתהליך העבודה, אבל בלי להפר את עקרון יישום פתוח-סגור!

ככה זה נראה:

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

אבל רגע, הבטחתי לכם בפוסט הקודם שאני לא אשאיר אתכם עם דוגמאות כלליות.
מכיוון שהובא לידיעתי שלא רק תוכניתני PHP קוראים את סדרת הפוסטים.. ומכיוון שאני רוצה שכולם יאהבו אותי – אני שמח להגיש לכם את הדוגמה ב – TypeScript!
ועל הדרך אאזכר ואקדם (בלי בושה) את מדריך ה – TypeScript שכתבתי בשני חלקים, לאלו מכם שעדיין לא למדו את השפה.

דוגמה ראשונה ליישום פתוח סגור ✅
יח"צ לעקרון הראשון ✅

נראה לי שאפשר לסיים את הפוסט כאן.. שכחתי משהו?

נכון! הבטחתי לכם עוד דוגמה; אז קבלו במחיאות כפיים (לא-סוערות-מידי-כי-המחלקה-הזאת-מפרה-את-עקרון-יישום-פתוח-סגור) את MasterValidator:

MasterValidator – אימות שדות טופס

פעולת האימות של הרכיב MasterValidator עובדת כך:

  1. הרכיב מקבל את קלט המשתמש
  2. הרכיב יוצר מופע חדש של סוג האימות שהוגדר
  3. הרכיב מפעיל את פעולת האימות

בואו נתחיל לכתוב את האפליקציה;
מתחילים עם יצירת עצם מסוג קלט:

בואו נבין מה מתרחש במחלקה Input:
המחלקה Input מיישמת את הממשק InputInterface; היא מכילה שלוש תכונות:

  • validator: any מקבלת את מחלקת האימות המתאימה
  • type: string מקבלת את סוג הקלט (text, radio וכו')
  • value: string מקבלת את הערך של הקלט

דיי ברור מה תפקידן של הפונקציות getType ו – getValue.
אבל מה קורה ב – constructor? הפונקציה מקבלת שלושה פרמטרים:

  • validator: string מקבלת את שם המאמת
  • type: string מקבלת את סוג הקלט
  • value: string מקבלת את ערך הקלט

בתוך הפונקציה, נעשית קריאה לפונקציה הפרטית buildValidatorName שלוקחת את שם המאמת, הופכת את האות הראשונה לאות גדולה ומצרפת אליו את המחרוזת Validator.
בחזרה ל – constructor: התכונה validator מקבלת באופן דינמי מופע חדש מסוג המאמת המתאים, באמצעות הפונקציה Object.create שמקבלת כפרמטר את אב-הטיפוס של המחלקה המתאימה (לחצו לקריאה נוספת).
שתי השורות הבאות מיישמות ערך לתכונות type ו – value בהתאמה -שום דבר מיוחד.

בנוסף עבור כל תכונה הוגדרה פונקציה שמושכת את המידע הרלוונטי – כפי שנדרש בעקבות יישום הממשק InputInterface.

אוקיי! יש לנו מחלקה שמתארת קלט וזה הצעד הראשון.
בואו נתקדם הלאה ונגדיר את המחלקות שמבצעות את האימות.

לצורך הדוגמה, כתבתי שתי מחלקות שכאלה שמבצעות אימות לשם ואימות למספר טלפון:

היישום עצמו פחות רלוונטי ולכן אציין רק שכפי שניתן לראות, לכל מחלקה יש פונקציה בשם test שמבצעת בדיקה האם המחרוזת תקינה ומחזירה ערך בוליאני בהתאם.

החלק האחרון בפאזל הוא המחלקה MasterValidator שמתפקדת כבקר:

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

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

ואתם צודקים! (לא בדיוק.. זו מלכודת) בקלות יכולתי לכתוב את הפונקציה check של MasterValidator ככה:

עכשיו, כל מה שעלי לעשות כדי להוסיף סוג אימות חדש, הוא לכתוב מחלקה, לקרוא לה בשם תקין (כלומר שתסתיים ב – Validator) ולרשום פונקציה בשם test שתבחן האם המחרוזת שהתקבלה תקינה ולהחזיר ערך בוליאני מתאים.

אבל מי אוכף את כל זה? אני? חוברת-הדרכה? תיעוד בקוד?
מטרת העל של עקרון יישום פתוח-סגור היא להעלות את רמת השרידות של תוכנה; כלומר לאפשר לה להתקיים לאורך זמן ולאור שינויים שמבוצעים בה תוך כדי שמירה על יכולת תחזוקה סבירה.
המטרה מושגת על-ידי כתיבה תבניתית שמאפשרת לנו לדעת מראש מהן אפשרויות הבסיס שכל מחלקה מציעה לנו.

אבל להחליט לכתוב באופן תבניתי – זה לא מספיק.
עקרון יישום פתוח-סגור דורש שנמנה מבוגר אחראי שידאג לאכוף את דרך הכתיבה שהגדרנו;
כל עוד לא קיים מנגנון שמבצע אכיפה אמיתית – עקרון יישום פתוח-סגור לא מתקיים.

בחזרה ל – MasterValidator, למרות שאנחנו יכולים להגיע למצב שבו אין צורך להוסיף שורת קוד אחת בשביל להוסיף סוג אימות (התנהגות חדשה),
אנחנו לא אוכפים מצב שכזה.

ממשק (interface) בעצם מגדיר עבור המיישם שלו דרישות סף שעליו למלא, אם המיישם לא יעמוד בדרישות הסף הללו – תתרחש שגיאה.
ולכן ממשק הוא-הוא אמצעי האכיפה של עקרון יישום פתוח-סגור. בואו ניישם את העקרון ברכיב MasterValidator:

קודם כל, נכתוב את הממשק שיגדיר את דרישות הסף לכל מאמת:

השלב השני הוא ליישם בכל מחלקה את הממשק ValidatorInterface.
מכיוון שהקוד נכתב בצורה אחידה, אנחנו נדרשים להוסיף לחתימת כל מחלקת אימות את ההכרזה על היישום:

לפני שאנחנו ממשיכים, בממשק InputInterface יש בעיה. ככה הוא נראה:

הגדרתי בו תכונה ציבורית (public) בשם validator. בטח יצא לכם לשמוע שתכונת מחלקה לא צריכה להיות ציבורית.
יש אמת באמירה הזו ובסוף הפוסט – מחכה לכם הסבר למה.

זה לא נגמר כאן. התכונה יכולה לקבל כל סוג נתונים (any), למרות שהדרישה בפועל היא שהיא תקבל מחלקת אימות.
מקודם, לא יכולנו לדעת מה סוג הנתונים שהתכונה תכיל, אבל עכשיו כשמיסדנו את העניינים עם הממשק ValidatorInterface, נתקן את העוולה הכפולה:

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

שימו לב לפונקציית הבנאי שמקבלת פרמטר מסוג ValidatorInterface ומיישמת אותו לתכונה validator, במקום לקבל מחרוזת ולנחש את שם המאמת.

אחרונה חביבה היא הפונקצייה clean של MasterValidator:

כעת הפונקציה מקבלת מראש רק את סוג הנתונים InputInterface והוספת מאמת חדש מצריכה אחת ורק את כתיבתו.

בונוס – סגנון טוב יותר לקוד טוב יותר

לא סתם עקרון יישום פתוח-סגור הוא בין 'חמשת העקרונות הראשונים' לעיצוב תוכנה מונחה עצמים.
הוא מביא לתוכנה יעילה יותר, יציבה יותר וגמישה יותר.

אבל זה לא הכל! לא מעט מוסכמות נוצרו בהשראת העקרון.
בחרתי לכתוב על שתיים כאלה, שישפרו הקוד שלכם על-ידי כתיבה נכונה יותר:

משתני מחלקה יוגדרו כ – private בלבד

הבטחתי ואקיים:
אולי החוק-הלא-רשמי הכי מפורסם שנולד מתוך העקרון: משתני מחלקה לעולם יוגדרו כ – private.
אני בטוח שהרוב הגדול של הקוראים שמע על זה. אבל עצרתם פעם לשאול למה?

בואו ניזכר שוב מה ההיגיון שמאחורי עקרון יישום פתוח-סגור:

הוספת התנהגות חדשה תיעשה על-ידי הוספת קוד חדש ולא על ידי שינוי הקוד הקיים

כשמשתנה מחלקה מוגדר כ – public, הוא חשוף לשינוי גם על-ידי כל מי שרק יחפוץ בכך.
כל שינוי שכזה יצריך מאיתנו לשנות גם את כל הפונקציות שמסתמכות על משתנה המחלקה, כלומר: לשנות את הקוד הקיים.
כשמשתנה מחלקה מוגדר כ – private, לעומת זאת, רק מחלקת האם יכולה לבצע בו שינויים.

משתנים גלובליים החוצה!

הטענה כלפי משתנים גלובליים דומה לזו נגד משתנים שמוגדרים כ- public: לעולם לא נוכל להבטיח שרכיב שמסתמך על משתנה שיכול להשתנות מכל מקום,
ימשיך לתפקד באותו האופן שבו תכננו אותו. פוטנציאל ההרס עצום!

כמעט בחצי הדרך..

כתיבת הפוסט לוותה בקשיים – הרגשתי אחריות כבדה להעביר את מלוא ההיגיון שנמצא מאחורי עקרון יישום פתוח-סגור, אני מקווה שהצלחתי במשימה.
2 עקרונות מתוך 5 כבר מאחורינו, נתראה בפעם הבאה עם העקרון השלישי: עקרון ההחלפה של ליסקוב

 

מקורות

אם הגעת עד לפה, הגיע הזמן להירשם לניוזלטר!

Summary
Article Name
עקרון יישום פתוח-סגור
Description
עקרון יישום פתוח-סגור, העקרון השני בקבוצת העקרונות S.O.L.I.D קריטי לכל מפתח שרוצה להיות מסוגל מערכת שתחזיק מעמד לאורך זמן תוך כדי תחזוקה והוספת פונקציונליות
Author
Publisher Name
MasterScripter
Publisher Logo
אודות 
hello world! אני יונתן, מתעסק בתכנות ופיתוח WEB פלוס מינוס מאז שאני זוכר את עצמי. ב2013 הקמתי את קבוצת סיזן ביחד השותף היקר שלי - שלומי שלומקה זק. ביום מתעסק בפיתוח ועיצוב אתרי אינטרנט ואפליקציות, עיצוב חוויית משתמש, הקמת מיזמים, ועוד..אבל מה שלא ידעתם עליי - זה שבלילה אני באטמן. אל תגלו לאף אחד!

6 תגובות

  1. משה

    26 בפברואר 2017 - 23:06
    תגובה

    תודה רבה מאמר מעולה !

  2. שמעון

    27 בפברואר 2017 - 8:32
    תגובה

    תודה. מאמר מעולה.

  3. אברהם

    1 במרץ 2017 - 19:40
    תגובה

    תודה רבה על ההשקעה!!
    מאמר יפה ובהיר מאד!!
    כיף שיש מי שמשקיע זמן כדי שיהיה קצת מה לקרוא בעברית.

  4. יואל

    17 במאי 2017 - 9:05
    תגובה

    מאמר יפה,
    למדנו את המאמר שלך במכללה (למרצה לא היה כוח להכין שיעור……..)

השאר/י תגובה

האימייל לא יוצג באתר. שדות החובה מסומנים *