יום שלישי, 18 ביוני 2013

שאלות על Object Oriented Desgin

לפני כשבוע נתקלתי בוויכוח הבא:


במערכת כלשהי, באזור לו נקרא "Sub-Project 3", מחלקה A (בג'אווה) קראה למתודה במחלקה B, אשר דרשה כפרמטר איזה ערך. הערך יכול להיות אחד מ 3 ערכים קבועים - ועל כן המפתחים יצרו enum (נקרא לו ENUM_X). עד כאן - יפה וטוב.

מפתח אחר גילה שבדיוק אותו enum (נקרא לו 'ENUM_X) מוגדר במקום אחר בפרויקט, ודרש שמחלקות A ו B ישתמשו ב enum המקורי - כך שלא יתוחזק "קוד כפול".
המפתח אשר כתב את הקוד במקור טען: "חבל לייצר reference לעוד תת פרויקט ב Build בשביל כזה דבר קטן. שיהיו שני enums זהים - לא יקרה שום דבר."

- "אבל אם תשנה אחד ותשכח את השני?! - מה אז?"

הוויכוח התלהט והגיע לראש הקבוצה (!).

מה דעתכם? במי אתם הייתם מצדדים?



כיצד לסיים את הוויכוח?

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

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

האם זהו הטיעון המנצח שיפתור את הדיון?



הממ... לא בטוח.
הנה טיעון מלומד אחר:

"אסור למודול להיות תלוי בחלקי-ממשק שאין לו בהם שימוש". במקרה שלנו יצרנו תלות לא רצויה בכל "Sub-Project 7" - כלומר בהרבה מחלקות וממשקים שאין לנו בהם שימוש. הממ... נשמע חמור!
עיקרון זה נקרא The Interface Segregation Principle.


האם ייתכן שעקרונות ה OOD סותרים זה את זה?





כמה שאלות

  • האם יכול אדם, המכיר את 2 העקרונות והוא בעל כושר שכנוע, להחליט באופן רגשי במי הוא מצדד וכל פעם לשלוף את "הטיעון התאורטי המתאים" בכדי להנחית "טיעון מנצח"? האם הוא יכול לעשות זאת מבלי להיות מודע לכך ולהאמין שהוא "רק פועל ע''פ התאוריה"?
  • בהינתן שחוקי ה OOD סותרים לעתים אחד-את-משנהו, האם ישנם חוקים "חזקים יותר" שיש להעדיף?
  • נניח ונוותר על אחד החוקים או שניהם - איזה "נזק" יתרחש? מה ההשלכות של "לא לציית לחוקים"? האם המאמץ הנוסף שבציות לחוקי ה OOD - משתלם?
  • האם OOD היא מתודולוגיה מוצלחת? האם, לאחר כל השינויים בשיטות העבודה שחלו בעשור האחרון - היא עדיין יעילה או רלוונטית?

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


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

כיום אני יודע לומר שנצמדתי במידה רבה, לזרם בתוכנה שנקרא "Defensive Programming". זרם ששפת ג'אווה ו JEE היו אולי רגע השיא שלו. הוא מתבטא ברעיונות כגון:
  • "על המתכנת צריך להגן על התוכנה בפני המפתחים - כולל הוא עצמו".
  • עשה כל מה שתוכל כדי להפחית סיכונים לבאגים. 
גישה זו יצרה הרבה משמעת (discipline), אך גם הובילה להמלצות כגון כתיבת מחלקה singleton בג'אווה בתוך enum על מנת להבטיח singleton "שפשוט אי אפשר לקלקל" [א].


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



עקרונות ה OOD - למבחן!


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


חלוקה הקוד למחלקות או מודולים
  • (The Single Responsibility Principle (SRP
  • (Don't Repeat Yourself Principle (DRY
  • Encapsulation
  • High-Cohesion / Low-coupling Principle
  • The Common Closure / Reuse Principle


הגדרת הפשטות (abstactions) / אינטראקציה בין מחלקות
  • The Open-Closed Principle
  • The Liskov Substitution Principle
  • The Release-Reuse Equivalency Principle
  • The Stable Abstraction Principle


ניהול תלויות (מחלקות עד מודולים במערכות)
  • The Interface Segregation Principle + גרסת הקוד שלו
  • (Single Layer Of Abstraction Principle (SLAP
  • The Dependency Inversion Principle
  • The Acyclic Dependencies Principle
  • The Stable Dependencies Principle

עדכון: עקרונות אחרים של תכנון מערכת:

עדכון 2: הנה איזו רשימה דומה של בחור אחר.

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

נראה לי שאבחר כמה מהעקרונות הנ"ל - ואתחיל לנתח אותם יותר לעומק.
הערות ומחשבות יתקבלו בשמחה.


ליאור


נ.ב: האא כן - איזה פתרון אני הצעתי לדילמה שהצגתי בתחילת הפוסט? אני הצעתי פשוט להשתמש במחרוזת (string) במקום enum וכך להימנע בכלל מתלויות. מה הייתרון של ENUM על מחרוזת פשוטה? Type Safety? ובכן... ה speller ב IDE ימצא את רוב שגיאות הכתיב. בדיקות-היחידה אמורות לתפוס את השאר...


----

[א] בספר Effective Java 2nd Edition, פריט מס' 3, ע"מ 18. אפשר להתייחס לכל סדרת ספרי ה ... Effective - כספרים של זרם ה "Defensive Programming".



19 תגובות:

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

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

    השבמחק
    תשובות
    1. אני מסכים איתך לגמרי, אבל יש לי הפתעה (אני חושב):
      מאז JDK 1.4 (?) ה JVM מחזיק POOL של ה string שנוצרו במערכת, כך שיש סיכוי גבוה כשאתה כותב hardcoded string בכמה מקומות - שזה יהיה אותו ה reference ולא יהיה צורך לבצע באמת השוואת מחרוזות אות אחר אות.

      לא הייתי בטוח ועל כן עשיתי בדיקה (ולכן רק עכשיו אני מגיב):
      http://ideone.com/IXycw5

      תוצאת הבדיקה:
      השאווה של enum עשר *מיליון* פעמים = 27ms
      השאווה של מחרוזת עשר *מיליון* פעמים (בשל השימוש ב equals שמייצר activation frame של פונקציה) = 111ms.

      לא משהו שהייתי מוטרד בגללו.

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

      תודה על התגובה.

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

    השבמחק
    תשובות
    1. אלו אינם 2 פרוייקטים שונים אלא 2 מודולים ("תתי פרוייקטים" הוא מונח שנבע מהשימוש ב Perforce, אני מניח) באותו הפרוייקט.

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


      אני מקווה שזה נשמע הגיוני עכשיו.

      מחק
  3. ואם הם עדיים מפחדים להשתמש בstring ניתן לשנות את שמו של הenum ל ENUM_Y.

    השבמחק
    תשובות
    1. נכון.

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

      פרספקטיבה: ה enum החדש היה בשימוש בנקודה מאוד ספציפית.

      מחק
  4. יתרון נוסף לשימוש ב-ENUM - אפשר לחפש שימושים בו בקוד (references), בעוד שימוש ב-STRING לא בהכרח יניב תוצאות.

    השבמחק
    תשובות
    1. האם חיפוש אחר "enumValue" (כולל המרכאות) ייתן תוצאות פחות טובות?

      סיכוי ל false positive פה ושם, אני מניח.

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

      מחק
  5. אהבתי מאוד. אכן שווה לחשוב מחדש ולאתגר את מה שאתה חושב שאתה יודע, זה הדרך לצמיחה מתמדת.

    לסוגיה בהתחלה - יש כמובן את האופציה של הוצאת ה-ENUM לCOMMON שכזה. אני מניח שנראה שפה זה לא היה שווה את המאמץ? ואתה יכול לשתף מה בסוף נבחר ולמה?

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

      ליאור

      מחק
  6. למה לא לשים את ה enum במחלקה נפרדת שתשב בפרוייקט שירותים שכל מי שצריך יוכל לעשות אליו refernce?

    השבמחק
    תשובות
    1. זו גם אפשרות.

      במקרה שלנו לא היה פרוייקט קיים שכזה והאפשרות לא עלתה.

      מחק
  7. אם מפתחים בלינוקס או מק, אני הייתי חושב על יצירה של link לקובץ שמגדיר את ה-ENUM, קרי,
    ln -s source_enum_file target_enum_file
    כאשר פותחים את כל אחד מהקבצים (כולל הלינק), הוא מביא את הקובץ המקורי היחיד ואפשר לעדכן מקור יחיד, והקומפיילר רואה אותם בתור קבצים שונים בספריות שונות.

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

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

      מחק
  8. טוב נו, אני חושב שקפטן ברבוסה ניסח את זה הכי טוב בדוברו על ה- Pirates Code:
    Code is “more what you’d call ‘guidelines’ than actual rules”.
    נראה שבמקרה זה הבעיה נעוצה בכך שה- enum המדובר אינו ננמצא ב- package משותף לשני המודולים.
    זה מאפיין של design לא אופטימלי או סתם code base שקיים מאוד הרבה שנים (החלטות שהיו נכונות בזמנו, מאבדות את נכונותן עקב שינוי המציאות).
    בצוות שלי אנו מתייחסים למקרים כאלה ודומיהם בתור הזדמנויות: מצד אחד, לממש את ה- feature החדש ומצד שני, לשפר את ה- design הקיים, בהתטם לאילוצים פרויקטליים. לא פעם יצא לנו לבצע refactoring יסודי שכל עיקרו הזזה של מחלקות/מודולים ממקום למקום, זאת ע"מ לקבל design יותר נקי ולהגדיל את אפשרויות ה- reuse.

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

    השבמחק
    תשובות
    1. היי,

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

      ליאור

      מחק
    2. מצטרף לבקשה זו

      לעניין הפוסט- אני הייתי מוציא את הENUM ל DLL חיצוני ועושה רפרנס אליו משני המודולים.

      מחק