2011-12-06

כיוונים חדשים-ישנים במתודולוגיות פיתוח תוכנה (Data-Oriented Programming)

תשאלו אנשים הכותבים מערכות ב #C או Java מהי מתודולוגית הפיתוח הנפוצה ביותר כיום וקרוב לוודאי שתשמעו "תכנות מונחה-עצמים, ברור!".

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

ובכן, לצערי רוב המערכות שראיתי (וראיתי) היו לא-מוצלחות בהיבטי ה Object-Oriented. כינויים נפוצים למערכות אלו הן:

  • Anemic Object Model - מצב שבו האובייקטים הם דלילים ולרוב מחזיקים רק נתונים או רק פונקציות.
  • או Big Ball Of Mud - "גוש גדול של בוץ" (שהאמת, מתייחס למגוון רחב יותר של בעיות).


רבים מאיתנו רוצים ליצור תוכנת Object-Oriented הבנויות לתפארת עם Domain Model עשיר, אך אנו נכשלים לעשות זאת שוב-ושוב. האם רוב המתכנתים בעולם גרועים? או שאולי מתודולוגית ה Object-Oriented אינה טובה? (השם ירחם - דברי כפירה)

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

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

פיתוח מונחה-נתונים (Data Oriented Programming)
בתכנות מונחה-עצמים, האובייקטים הם במרכז ובתכנות פרוצדורלי - הפונקציה היא במרכז. בתכנות מונחה-נתונים (DOP מותר לבטא כ Dope) תשומת הלב נעה מן הקוד לכיוון הנתונים: מהו הקלט, איזה טרנספורמציות (עיבוד) המידע עובר וכיצד הוא נראה בסוף.

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

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

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

על האנומליה של הזכרון
בוודאי למדתם באוניברסיטה קורס "מבני נתונים" - קורס חשוב המכסה חומר לא טריוויאלי.
למדתם שיש רשימה משורשרת עם הכנסה של (O(1 ו"טיול" (traversal) של (O(n, ויש וקטור (נקרא ArrayList ב #C וג'אווה) עם מחיר הכנסה של (O(1 או (O(n (עבור הכנסה באמצע או מילוי המחסנית שהוקצה) ו"טיול" (traversal) של (O(n.

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

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

פיזור מקובל של תפוסת-זכרון של רשימה משורשרת (באדום). מקור: תוכנת ה disk defrag שלי ;-)

כאשר אנו מקצים זכרון לרשימה משורשרת, האלמנטים בה יתפזרו על גבי הזכרון באופן אקראי, ע"פ המקום הפנוי באותו הרגע (כמו הבלוקים האדומם בתרשים למעלה). לעומת זאת הקצאת זכרון של מערך (כלומר וקטור) תהיה רציפה וללא חלקים. היכן זה משנה לנו? כאשר "נטייל" על הרשימה:
  • אנו יודעים שמערכת ההפעלה עובדת עם Virtual Memory. אם בלוקים של זכרון בהם שמור אלמנט אחד לפחות מהרשימה שלנו הם paged out (כלומר נשמרו לדיסק על מנת לשחרר זכרון פיסי), מערכת ההפעלה תקבל Page Fault שיגרור Context Switch וטעינת הדף / כתיבת דפים אחרים לדיסק - פעולה יקרה!
  • זכרון המטמון (בעיקר L2 ו L3) במעבד נוטים לעשות prefetch ואחזקה של בלוקים של זכרון - לא תאי זכרון בודדים. כאשר אנו משתמשים בזכרון רציף גישה זו תהיה מועילה, אך עבור רשימה משורשרת היא יכולה אפילו להזיק ולבצע prefetch לזכרון לא רלוונטי [1].
"אבל זכרון הוא נורא מהיר!" - אתם עלולים לטעון. "אנו יודעים שעבור ביצועים-גבוהים יש לבצע הכל בזכרון". ובכן יחסית לפעולות IO זה נכון - אבל יש גם הבדל בין שימוש בזכרון כאשר זכרון המטמון יעיל או כאשר הוא לא יעיל.

במשך 30 השנים האחרונות - המעבדים הלכו והפכו מהירים עוד ועוד , משמעותית מהר יותר מהקצב בו התפתח הזכרון. אם ב 1980 המעבד המתין Cycle אחד לקריאה מהזכרון, היום הוא ממתין בערך 400 Cycles. הבעיה מחמירה כאשר הזכרון הזמין גדל (המעבר לעבדי 64 ביט פרץ את גבולות 4GB זכרון) ואנו רוצים להשתמש בזכרון על מנת לעבד כמות רבה יותר של נתונים - בעיה הידועה כ"צוואר הבקבוק של פון-ניומן". מעבדים מודרניים יודעים לעבוד עם Bus רחב בהרבה לזכרון, כלומר קריאה של יותר ביטים במקביל שמושגת ע"י קריאה (וכתיבה) במקביל מ 2 עד 4 יחידות זכרון (עקרון שדומה מאוד ל RAID 0 בכוננים קשיחים).
שיפורים בביצועי המעבד מול הזכרון ב30 בשנים האחרונות. מקור: Computer architecture: a quantitative approach

כיצד מתכנתים ב Data Oriented Programming
אלו שפות הן שפות "Data Oriented"? ובכן, השפה היחידה שאני יכול לחשוב עליה ככזו היא SQL: אנו מודעים לטבלאות וחושבים בפעולות על הנתונים. לטבלה B יש Foreign Key המצביע על טבלה A? אנו יכולים לבצע Select על טבלה B ולמצוא את כל ההצבעות - אין צורך (או משמעות גדולה) בהצבעה כפולה (כמו באובייקטים). ב #C יש את LINQ שהיא אמנם שפה לטיפול בנתונים, אך טבעית מאוד גם לנתונים במודל אובייקטלי - לכן אני לא בטוח שהיא דוגמא טובה.

העקרון של תכנות מונחה נתונים Per-Se הוא להחזיק נתונים (כאשר יש רבים מהם) בזכרון בצמידות. ממש כמו שמירה של טבלאות של בסיס נתונים רלציוני. כך יהיו לנו הרבה Collections גלובאליים של "אובייקטים", כאשר האובייקטים הם רזים (יותר דומים ל struct של נתונים ופחות אובייקטים קטנים ועשירים). מצד שני יהיה ניתן להפעיל כל פעם פונקציה אחרת על אותו struct - מעבר שהוא "זול" מבחינת עלות זכרון (מכיוון שיש יחסית מעט פונקציות נפוצות שיכולות להשמר ב Cache).
בעוד תכנות מונחה-עצמים יוצר מבנה דומה ל LinkedList - אובייקטים הפזורים לכל עבר בזכרון, כאשר קריאות getX.GetY.GeyZ המפורסמות של ג'אווה מדלגות בזכרון לא-רציף, תכנון מונחה-נתונים הוא דומה יותר לוקטור (ArrayList) רציף בזכרון המאפשר להשתמש ב Cache בצורה יעילה ופונציות שונות שפועלות בצורה ממוקדת על הנתונים ללא "קפיצה" תכופה לאובייקטים אחרים.

השימוש בData-Oriented Programming כמתודולוגיה מקובלת חזר בשניים האחרונות בעקבות אפליקציות ה Mobile (כלומר Android, iPad, iPhone) - אפליקציות קטנות הנכתבות עבור מכשירים בעלי כח עיבוד חלש.
מתכנתים מדווחים על ביצועים גבוהים פי 2 עד 4 בהמרה של אפליקציות Object Oriented להיות אפליקציות Data Oriented ובקלות פיתוח גבוהה יותר.

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

כמו שאנשים חששו להשתמש ב"אובייקט פשוט של ג'אווה" (והשתמשו ב EJB) עד שניתן לו השם המכובד POJO, כך גם אין לחשוש בתכנות מונחה נתונים ולהתייסר שאנו לא כותבים OOP, הנה עכשיו יש לו שם מכובד: DOP.
בסופו של דבר כל אלו הם כלים, עם יתרונות וחסרונות, ומקצוען אמיתי ידע לבחור בניהם בחוכמה ולא ייקלע למלכודת של "חייבים לעבוד ב EJB או OOP או <כלי אחר> - כי כולם עובדים כך".
דוגמא קרובה נוספת היא ההצלחה של REST - מודל פשוט וממוקד נתונים / פרוטוקול רשת, שהצליח יותר בשטח ממודלים מונחי אובייקטים או שירותים, ארכיטקטורות נבונות מגובות בהמון תאוריה.

אני חושב שאפשר בהחלט לכתוב מודולים במערכת שהיו מונחי נתונים, ועדיף שזה יהיה מהלך מודע. תכנות מונחה-עצמים הוא בהחלט לא קדוש.
גישה אחת ל DOP היא הגישה הקלאסית[2] (מערכיים של structs) וגישה אחרת היא עבודה עם בסיס נתונים רלציוני בזכרון (כגון H2, HSQLDB או SQLite), עם היכולת לשמור את הנתונים לדיסק בקלות ובכל רגע.

היתרונות של DOP על OOP
אני מניח שהיתרונות של OOP (הכמסה, מידול, ריבוי-צורות ועוד) מוכרים לכולם, בואו נסקור כמה יתרונות של ה DOP:

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

Unit Testing
מי שעובד עם unit tests יודע שהכי קל לכתוב בדיקות לפונקציות המרה פשוטות של נתונים, כמו פעולות parsing, למשל. שימוש ב DOP יהפוך גם את ה unit tests לפשוט וטבעי יותר מכיוון שיהיו הרבה יותר פונקציות ש"רק מעבדות נתונים" ויפחית משמעותית את הצורך ב mocks, stubs וחברים. אימוץ Unit Tests הוא לא דבר קל, ושימוש ב DOP יכול להיות סיוע משמעותי.

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

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

מאגר אדיר של בסיסי נתונים (רלציוניים) התואמים למודל כמו כפפה.
מודל ה OOP לא רק הקשה על החשיבה הלוגית שלנו ועל ארגון התוכנה לאובייקטים, הוא גם הקשה מאוד על שימוש בבסיסי הנתונים הרלציוניים, אשר בטבעם הם מונחי-נתונים.
במשך שנים ניסו לבנות כלי (ORM (Object-Relational Mapping עם הצלחה חלקית בלבד. אפילו Hibernate/NHibernate - ה framework הפופולארי ביותר, הוא נחמד לשמירת קונפיגורציה אך כושל כאשר אנו זקוקים לביצועים טובים על הרבה נתונים. גם אני האמנתי למצגות שמספרות שברוב המקרים Hibernate יבנה סכמה יעילה יותר מהמתכנת ויספק Cache שישפר את הביצועים אפילו יותר. הנסיון שלי הוא שכאשר יש דרישה לביצועים טובים, ישנו מאבק ארוך עם Hibernate שבסופו Hibernate מוצא את עצמו מחוץ למשחק.
אמנם אם היינו יוצרים באופן ידני את אותה הסכמה ש Hibernate מייצר - הוא יכול היה להיות מהיר יותר, אך ההבנה שלנו בנתונים מובילה אותנו לסכמות שונות לחלוטין ממה ש Hibernate ייצר.
אחד המניעים של תנועת ה NoSQL היא השתחררות ממיפוי אובייקטים למודל רלציוני ועבודה ישירות מקוד OO למודל שמירת נתונים OO (כגון בסיסי נתונים KV). על אותו מטבע אם הקוד שלנו הוא מונחה-נתונים, כך גם בסיסי נתונים רלציונים ישרתו אותנו היטב ובקלות - ויש המון כאלה.

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

היתרונות של חלוקה של מערכת למודולים עם אחריויות ברורות, הכמסה נוקשה (encapsulation) של כל מודול ותיאור המערכת כשיקוף של העולם האמיתי / הבעיה העסקית (מה שנקרא גם DDD - Domain Driven Design) - הם יתרונות ברורים שעובדים היטב בשטח.

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

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

בהצלחה.


[1] בשקפים 31 עד 101 של מצגת זו - ניתן למצוא הסבר מפורט של התופעה.

[2] יש לזכור שמערך בג'אווה הוא רשימה רציפה של מצביעים. האובייקטים עצמם (אליהם אנו מצביעים מה ArrayList) עדיין מפוזרים באופן שרירותי בחלק הזכרון הנקרא Heap. ב #C המצב נוח יותר - יש structs שיכולים להיות מוגדר באופן רציף. בכל מקרה אני רוצה להדגיש ולחזור שאופטימזציית performance היא לא עצם העניין, עצם העניין הוא מודל שקל יותר לשימוש המפתח - אופטימזציות הביצועים בדיון זה היא משנית.

7 תגובות:

  1. רשימה מקושרת אינה מחייבת פיזור בזיכרון.

    השבמחק
  2. אני מבין שאתה מדבר על custom made Link-List וזו נקודה טובה.

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

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

    השבמחק
  4. אנונימי4/3/12 14:06

    רשימה מקושרת ניתנת למימוש באמצעות מערך !

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

    בהנחה שנעשה שימוש בזכרון דינמי.

    השבמחק
  5. נכון. אני מניח שמשתמשים במימוש כמו LinkedList. ע"פ נסיוני זה מה שיקרה ברוב הפעמים - כנראה מחוסר מודעות.

    השבמחק
  6. אנונימי16/1/13 11:20

    אילו שפות קיימות בפועל לתכנות DOP?

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

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

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

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

      ליאור

      מחק