2019-06-30

על פרדיגמות תכנות, ומה ניתן ללמוד מהן בשנת 2019?


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

בתכנות ישנן כמה פרדיגמות תכנות מקובלות. הפרדיגמות המשפיעות ביותר כיום הן פרדיגמת ה Objection-Orientation (בקיצור: OO) ו Functional Programming (תכנות פונקציונלי) - אך גם יש אחרות מוכרות יותר ופחות.


באיזה פרדיגמה אתם מאמינים? עם איזו פרדיגמה אתם עובדים?

עבור הרוב המוחץ של אנשי-התוכנה, התשובה היא זו:

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

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



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


מה עדיף?



איזו פרדיגמה היא הטובה ביותר?

אולי OO? - שהיא (עדיין) הדומיננטית, והמשפיעה ביותר ב 20-30 שנה האחרונות?
ואלי תכנות פונקציונלי שמרגיש חדש יותר (הוא בעצם קדם ל OO) - וזוכה להרבה מומנטום מחודש בעשור האחרון?


אם אנחנו הולכים לכתוב קוד קצר בן כמה עשרות שורות קוד - שימוש ב OO, או תכנות פונקציונלי, או Reactive Programming - יהיה סיבוך ובזבוז זמן!

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

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

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


בעצם, משם הכל התחיל [א]: מפרדיגמות מאוד פשוטות, עבור תוכנות פשוטות, שרצו על חומרה - שלא יכלה לתמוך בתוכנות מורכבות יותר:



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

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


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

למשל: בתיאורים המקודמים של Alan Key ל Object-Oriented - יש דגש גדול על העיקרון ש"אובייקטים מתקשרים ע"י שליחת וקבלת הודעות". אפשר להסביר למה ההדגשות הללו היו חשובות בזמנו, ומה משמעותן, אבל היום ההגדרות הללו נכנסות תחת הגדרת ההכמסה (Encapsulation). כלומר: אובייקט שולח הודעות - כי הוא לא יכול לגשת לנתונים הפנימיים של האובייקט האחר בעצמו. בכלל, המונח "שליחת הודעות" מתקשר היום חזק יותר לפרדיגמה של Event-Driven - וזה כבר נושא אחר.



הדברים מסתבכים


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

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

בשלב מסוים, בסוף שנות השישים, נדמה היה שעולם התוכנה נתקע - ואולי לא יהיה ניתן לפתח תוכנות גדולות ומורכבות בזמן הקרוב. התוכנות הגדולות שניסו לכתוב, הסתבכו בצורה כ"כ קשה ולא צפויה (באגים, שינויים קשים לביצוע, הסתבכות לא צפויה של פרויקטים שמתארכים עוד ועוד) - שהועלה החשש שתחום התוכנה עשוי עשוי להיעצר. קראו לזה אז "The Software Crisis" - ואנו חווים וריאציות של האתגרים הללו עד היום.

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

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

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

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


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

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

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

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

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

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


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

הדיונים על נכונות השימוש בפרדיגמה הם כמובן חשובים: לא כל פרדיגמה מורכבת יותר - היא אכן טובה יותר. לאימוץ פרדיגמות יש מחיר ניכר, ולא כל פרדיגמה ש"תפסה" בארגון X - תצליח בהכרח גם בארגון Y. למשל: ב C עדיין משתמשים במשפטי goto - בעיקר בשם היעילות הגבוהה (קרבה למעבד).
בקרנל של לינוקס, למשל, יש כ 13K משפטי goto (בדקתי הרגע).

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

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

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

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

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

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

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

הורשה מרובה, למשל - היא לא בהכרח דבר רע, וב 90% מהפעמים - אפשר להשתמש בה בצורה נשלטת. אני זוכר שבעבודה עם ++C היו שטענו שכל הפחד מהורשה-מרובה - הוא מוגזם.
10% מהפעמים שבהם היא יצאה משליטה - היו מספיק קשות -- שהתעשייה (!) באופן גורף החליטה לזנוח את האפשרות הזו.

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

גם היום, עדיין יש בשפות, וב Frameworks יכולות בעייתיות: שמדי פעם מקצרות את הקוד - אבל בפעמים אחרות הן זרז לבעיה. מדוע חשיש יצא מחוץ לחוק ואלכוהול לא? - קשה לומר.

חשוב לזהות אלמנטים במערכת שגורמים לבעיות רבות מדי - ולחסום אותם. אני בטוח שיהיו מתנגדים, אבל זכרו שמערכות לא הופכות עם הזמן לפשוטות יותר, או קלות יותר לתחזוקה. הסתמכות על משמעת ודיוק של כל וכל מהנדס במערכת - היא מתכון לכישלון.
על זה נאמר: "It must be simple - or it simply won't be"




?Monty Python: What have the Romans ever done for us
(זכורה לי טענה שזה במקור דיון שהופיע בתלמוד -- אבל לא מצאתי מקורות המאשרים זאת)


אז מה הפרדיגמות המוקדמות תרמו לנו?



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



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

למשל: המצאת ה"בלוק". בתחילה - לא היה דבר כזה.

למשל: משפטי if היו יכולים באותה התקופה להפעיל רק ביטוי (expression) יחיד. לו היו 4 פעולות שלא היינו רוצים שיפעלו אם x הוא שלילי - היה עלינו לכתוב 4 משפטי if שכל אחד חוזר על הבדיקה "האם x שלילי" ואז מבצע עוד פעולה.

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

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

בגרסאות הראשונות של GOTO (כך מספרים), הוספת שורה בתחילת התוכנית - אלצה את המכנת לעבור על כל הקוד ולעדכן את מספרי השורות בכל משפטי ה GOTO - כי הם פשוט השתנו. רק אח"כ הוסיפו את ה Labels או מספרי שורות ברמת השפה.

סיזיפי? סיזיפי אך אפשרי? - כך נראתה החלוציות בתחום התוכנה...

העיקרון המרכזי של התכנות המובנה הוא לקבע סדר וכללים ברורים - איך "קופצים" בין שורות בתוך התוכנה. במקום ציון השורה (או Label שמייצג אותה) - יש הפשטות כגון לולאה, בלוקים, רקורסיה, או Sub-routines (שזו ההתחלה של פונקציות. עדיין בלי פרמטרים וערך החזרה ברורים - אלא על בסיס משתנים גלובליים).

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


ארצה להדגיש שני עקרונות שצצו מהגישה הזו ושווים דיון, גם ממרום שנת 2019:


Scoping [ב]

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

בתחילה היה קיים רק המרחב הגלובלי, אך בשל התנגשויות / טעויות שנבעו מכך - החלו להגדיר scopes מצומצמים יותר.

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

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

היום, בעולם ה IDE המתקדם - המשמעות המידית של scoping היא מה יציע לנו ה IDE ב Auto-complete. אם נגדיר הכל ב Scope הגלובלי - כל הקלדה תציע לנו את כל מה שיש.
אם נקפיד על הגדרת ישויות ב scopes מצומצמים ככל הניתן - הצעות ה Auto-complete יהפכו לממוקדות, והרבה יותר רלוונטיות.

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

מתלבטים מה עדיף?
- אני לא.

שווה לציין ש Scoping הוא צעד ראשון בכיוון של Information hiding ו Encapsulation. שוב ושוב עלו, לאורך ההיסטוריה - רעיונות בכיוון הזה.






Single Exit Principle

בכדי להגדיר מהו מבנה צפוי ומנוהל של רצף הרצת התוכנה, ולהפסיק "לקפוץ לשורה אחרת בקוד", הגדירו בפרדיגמת התכנות המובנה את "עיקרון היציאה האחידה" (מפונקציה). שם נוסף:"Single Entry, Single Exit". לכל בלוק קוד (=פונקציה) צריך להיות רק מקום אחד להיכנס דרכו - ורק מקום אחד שניתן לצאת ממנו.


העיקרון הזה הוא מובנה בשפות שאנו עובדים איתן - עם כמה חריגות. קשה מאוד להסתדר ב 100% מהמקרים עם Single Exit יחיד.

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

הנה דוגמאות עיקריות:
  • Exceptions שנזרקים - הם לא Single Exit.
  • continue או break בלולאה ל Label (בג'אווה, למשל) - הם לא Single Exit.
  • אפשר גם לטעון ש break ו continue באופן כללי - הם לא Single Exit, לא ברמת הבלוק.
    • בשפות פונקציונליות - לעתים אין להם מקבילה.
  • coroutines (על עוד לא ממתינים באותו הבלוק לסיומם) - הם גם לא Single Exit. הפעלנו קוד - שרק בונה-עולם יודע מתי בדיוק הוא יסתיים.
  • אחרון וחביב: יש גם מי שרואים בקריאת return מתוך גוף הפונקציה - הפרה של עקרון ה Single Exit. אמנם נקודת היציאה נתונה וקבועה - אך דילגנו על קוד - להגיע אליה. הטענה העיקרית - היא שזה לא צפוי. אני מצפה שהפונקציה תגיע תמיד לשורתה האחרונה.

אין לי פה איזה המלצה או מסר גורף לגבי רעיון ה Single Exist - אבל אני חושב ששווה להכיר אותו.
אני, אישית, שמח שיש break ו continue בלולאות. אני מרגיש נוח להשתמש ב return מתוך גוף של פונקציה (יש לזה מחיר מסוים - אבל האלטרנטיבה פחות טובה לדעתי).




סיכום



שוב לא הצלחתי לסיים את הפוסט ב 400 מילים.

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

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

שיהיה לנו בהצלחה!



----

[א] דילגתי עם קוד מכונה, אסמבלי, ועל הדיון של המכונה של טיורינג לזו של פון-ניומן. זו היסטוריה low level מדי, ולא מעניינת לדיון.

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


2019-06-01

שיעור מס' 8 בהורשה


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

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

לצורך הדיון אני אציג רק ממשקים, ולא אתעכב על חלוקה ל Interfaces ו Concrete classes (שהוא באמת משני לדיון).


בואו נתחיל.


הבעיה


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


מיקום הצורה (x, y), לצורך הדיון - הוא לא חלק מההגדרה.

משהו לא מריח כאן טוב בהגדרה הזו...
יש משהו מאוד פרוצדורלי, ולא כ"כ OO בהגדרה הזו: ה Path נראה כמו מרכיב זיהוי מרכזי בהתייחסות לצורה - אבל הוא מיוצג כתכונה כשאר התכונות.

למשל: אנו יכולים לייצר Line עם fillColor - מצב לא תקין. הכוונה להשתמש ב fillColor היא רק עבור פוליגון.

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

כיצד ניתן למדל את האובייקט בצורה יותר נכונה?

יש לכם איזה רעיון?!



פתרון ראשון




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

בואו נבחן את המודל שלנו עכשיו:
  • DRY (חוסר חזרה על הקוד) - מצב מצוין. אין באמת הרבה מה לקצר את הקוד.
  • הכמסה (encapsulation) - מצב בינוני. על הפשטת העל יש כמעט את כל התכונות, מה שאומר שכל מי ש"אוחז" ברפרנס ל BaseShape יקבל גישה לכל התכונות הללו. האם יש לזה צורך? או האם "שיתפנו" יותר מדי?
    הנחת היסוד היא שהשימוש האחיד באובייקטים הוא בעזרת הפונקציה ()draw. נניח: רוצים לרוץ על מערך ולצייר את כולם, אך בכל מקרה, כל גישה מעמיקה יותר דורשת את בדיקת הטיפוס ו downcasting. 
    • אין נזק מיידי באי-הכמסה ("אז שלא ישתמשו בזה! מה הבעיה?!") - אבל זה זרז לשימוש לא מבוקר בתכונות הללו. מפתח שמקבל הצעה לשימוש בתכונה ב autocomplete של ה IDE - לעתים רחוקות ישאל את עצמו אם נכון היה שהאובייקט יחשוף תכונה זו, או האם בכלל נכון להשתמש בה.
    • כל מפתח יכול להיכנס לאובייקט ולשנות את כל שדות האובייקט ל Public - אבל זה סייג המעורר הערכה מחודשת על נכונות הפעולה (ברוב, התקין, של המקרים).
  •  אמינות המודל למציאות / פשטות הבנה (presentation) - הרבה יותר טוב, אך עדיין ישנם עיוותים. 
    • הסרנו את ה Path כתכונה, ופתרנו את הבעיה בה fillColor זמין להיכן שאינו רלוונטי - שזה מצוין. 
    • שמות המשתנים אינם טובים. הם "פשרה" עבור "שימוש חוזר בקוד" / או סתם מכח האינרציה - פשרה לא טובה.

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

  • אנו מספקים את הצורך הבסיסי של המערכת ביישות לאחסן בה נתונים / להגדיר פונקציות. לחלופין זה היה יכול להיות מערך גלובאלי של נתונים. זו חובה כדי שהקוד יעבוד - אך זה קו האפס מבחינת הנדסת תוכנה.
  • אנו מתארים כוונה / רעיון - שאנו רוצים להנחיל לשותפנו לבסיס-הקוד (להלן Presentation), ובתקווה שקבענו את הכוונה / רעיון בצורה טובה ואמינה לעולם העסקי הרלוונטי.
  • אנו מגבילים את חשיפת הידע במערכת, בעזרת הכמסה - אם אנחנו עובדים לפי מודל Object Oriented.
  • כן, פרקטיקה טובה של קוד, היא לא לשכפל קוד (DRY = Don't Repeat Yourself) - אבל זו לא ממש מטרה בהגדרת אובייקט.

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

כדי לחדד מדוע האובייקטים בדוגמה שלנו אינם ממודלים בצורה טובה, נשטח שנייה את האובייקטים מהיררכיית שבה הם בנויים, ונציג אותם כפי שיופיעו ב run-time, או למשל - ב IDE כאשר אנו מקבלים המלצות שימוש ב autocomplete:


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

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

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

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

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


פתרון שני



אתם בוודאי רואים שהאובייקטים מתוארים עכשיו בצרה טובה יותר, כאשר אנחנו מנתקים מעט מהקשר ביניהם - ומגדירים אותם אחד אחד.

עבור Point, הרבה יותר מדויק להסביר שתכונה מסוימת היא קוטר (או לחלופין זה יכול היה להיות רדיוס) - ולא "line width", שמשאיר מקום לפרשנות. בוודאי שיותר טבעי לי לחשוב על color ולא "line color" - כי אין קו בנקודה.
ב Polygon יותר מדויק לדבר על Border ולא Line, עניין של דייקנות - שיכולה להשתלם מאוד לאורך חיי-מערכת.

שיתפנו בין האובייקטים רק את מה שהכרחי: הפונקציה ()draw (לצורך ריבוי-צורות).

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

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

היתרון היחידי של הפתרון הקודם היה צמצום של כמה שורות קוד - כאין וכאפס מול מודל ברור יותר.

ממשקים הוא כלי שתכליתו היא PolyMorphism (ריבוי-צורות): התייחסות אחידה לאובייקטים שונים ע"פ מכנה משותף משמעותי. אל תתלו עליהם משימות של Code-Reuse, כי זה לרוב ייגמר בפשרה לא-משתלמת..

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



סיכום


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

DRY היא פרקטיקה חשובה מאוד - אבל יותר באזור של תוכן הפונקציות: יש כמה שורות קוד שחוזרות על עצמן? הקפידו להוציא אותן לפונקציה משותפת.
יש משתנים כפולים (או state כפול כלשהו)? אבוי - היפטרו מהעותקים מיד! ("data duplication is the mother of all Evil").

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


ייתכן ואתם שואלים את עצמכם "אם זה שיעור מס' 8 בהורשה - מהו שיעור 7? ". טוב, נו - זו לא הייתה כותרת מדויקת. ניסיתי לבטא שזה שיעור חשוב ב Object-Oriented (ולאו דווקא הורשה), אבל לא השיעור הראשון והחשוב ביותר. וגם לא הבסיסי ביותר. אני מדמיין אותו ... כאיפשהוא כשיעור מספר 8, מבלי מחויבות גבוהה לדיוק.


שיהיה בהצלחה!