כיוונים חדשים-ישנים במתודולוגיות פיתוח תוכנה (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 היא לא עצם העניין, עצם העניין הוא מודל שקל יותר לשימוש המפתח – אופטימזציות הביצועים בדיון זה היא משנית.

שיקולים בתכנון מקביליות: Beyond Threads

מקביליות (concurrency) מתורגמת ע”י לא מעט אנשים ל Thread ו synchronized (בג’אווה) – דבר שהוא נכון, אבל מסתיר כמה אלטרנטיבות חשובות.

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

Thread נתפס כ”אמצעי להאצת התוכנה”, אבל זה לא בדיוק נכון. אם יש לי משימה מקבילית שכוללת הרבה I/O (כגון client להורדת קבצים מהאינטרנט) הגדרת thread לכל קובץ או segment שמורד היא הדרך הקלה לפיתוח, אבל לא הדרך היעילה.

דרך יעילה יותר היא הגדרת thread יחיד שעובד עם ערוצים רבים של IO אסינכרוני (כגון channels בספרית java.nio. מקביל לפקודת select ב C של unix/linux, למי שמכיר). בגישה זו יש thread יחיד שמאזין לרשימת ערוצי ה IO הרלוונטים (sockets מאתר ההורדות + streams לכתיבה לקבצים) וכל פעם שערוץ IO זמין לקבל פקודה (התקבל packet ברשת או נסתיימה כתיבה של באפר לדיסק)ה thread שלנו יתעורר ע”י event. עליו לעשות איטרציה ולבדוק איזה ערוצ(י)  מוכנים, לבצע את הפעולה ולחזור לישון עד פעולת ה I/O הבאה שהסתיימה.

אז מה חסכנו בעבודה עם thread יחיד (בסדר עולה):

  • יצירה של thread היא פעולה יקרה (thread pool עוזר להתמודד)
  • תזמון ה threads השונים הוא overhead.
  • לcontext switch בין threads יש מחיר.

כתיבה ב Thread אחד היא בהחלט יותר יעילה! בכל זאת ברוב הפרוייקטים הייתי מעדיף לעבוד עם מספר threads. דוגמת אפליקציית ההורדות היא דוגמא פשוטה במיוחד שבה כל ה threads הם אחידים, אבל לרוב המצב יותר מורכב. כמות הרווח מ thread יחיד תתרום, נאמר, 5% לביצועי המערכת? לא שווה ברוב המקרים לייצר קוד מסובך בשביל שיפור שכזה.
סיכום: thread יחיד היא אופטימיזציה טובה למקרים כמו המתואר לעיל.

היבט חשוב נוסף הוא מערכת multi-core שאותה ניתן לנצל רק עם מספר threads שיתאים למספר ה cores.
אם הייתי מריץ את המערכת הנ”ל על מערכת עם ארבעה cores (וייתכן היה שיש לי מספיק ערוצי IO להעסיק את ה thread עד תום) – הייתי רוצה 4 threads שינצלו את ה CPU עד הסוף. דוגמא קלאסית היא WinZip שעובד עם thread בודד מול תוכנות דומות (winrar, 7zip) שעובדות עם מספר threads:

אפרופו multi-core: הנוסחא המקובלת לבחור כמה threads לייצר ליעילות מרבית היא:

# threads = # of cores / (1 - blocking coefficient)
כאשר המקדם (Blocking Coefficient) הוא ערך מ 0 עד 1 כמה אחוז מהזמן ה thread ישן בין פעולות IO. אם יש לי מערכת עם 4 cores שישנים 30% מהזמן, אני ארצה לעבוד עם 6 threads.
Runtime.getRuntime().availableProcessors();
ייתן לי בג’אווה את מספר הcores הלוגים (יתחשב ב hyperthreading).

אסטרטגיות מקביליות

אחד הדברים שכן כואבים במספר threads הוא עלות הסינכרון. אם לא נסנכרן – יכולים להיות שיבוש מידע, deadlocks ו livelocks. אם אנחנו מסנכרנים, אז המחיר הוא בביצועים: כל lock שמגיעים אליו n threads גורם לכך ש n-1 יאלצו “לישון” למרות שהם רוצים לעבוד. דמיינו בעבודה את קבצי ה excel שמישהו פותח ואף אחד לא יכול לעבוד עליהם (כי הם נעולים), אבל במקום לעבור למשימה אחרת – כל מי שניסה לפתוח את הקובץ חייב ללכת לישון! (ועד עובדי חברת החשמל – רעיון לעיונכם). יצירת המון threads במקום לרוב לא תשפר את המצב: תשלמו הרבה על context switch בלי לפתור את הבעיה המהותית.

יתרה מכך, סינכרון חוסם את מידת ה scalability האפשרי בריבוי cores.
בהינתן מערכת ש 5% מזמן הביצוע שלה הוא קטע מסונכרן, אפילו אם יהיה לי את ה banana bridge i9-9990EX של אינטל שיצא ב 2024 עם 6400 cores, לא אוכל להשיג יותר מפי 5 ביצועים מאשר על מעבד ה i5 ארבעה cores הסטנדרטי שלי (בהנחה שלא היה חיזוק כוחו של כל core ושזו משימה יחידה שאני מריץ). נשמע דיי מאכזב למי שמתכוון לחכות לbanana bridge מעכשיו.

עקרון זה ידוע כ Amdahl’s Law וניתן לקרוא עוד עליו כאן. הפיתרון הוא לצמצם את כמות הסינכרון למינימום.

אז איך מפחיתים את כמות הסינכרון למינימום? ישנן מספר אסטרטגיות להתמודדות עם סנכרון:

  • mutable synchronization
  • isolated mutability
  • pure immutability
  • actors

mutable synchronization
זו הגישה שרובנו בוחרים בד”כ באופן אוטומטי בלי לשים לב. העקרון הוא לשים synchronized על כל מתודה שיש בה שינוי state שיכול להשתנות בין threads. זו הגישה הקלה והפחות יעילה. דרך שיפור מהירה היא לכתוב את ה synchronized בבלוקים הכי קטנים שאפשר ולא בחתימת המתודה (למי שלא מכיר – תנסו – אפשר לכתוב אותם אפילו על שורה בודדת).

isolated mutability
גישה זו היא צעד אחד הלאה, לצמצם את כמות הסינכרון למינימום – שזה המשתנים עצמם. במקום לעבוד עם Long אני אעבוד עם AtomicLong של java.util.concurrent שמספק לי פעולות אטומיות כגון getAndIncrement או incrementAndGet
שימוש בהן יאפשר לי לצמצם את הסינכרון לנתונים עצמם ולא מעבר.

pure immutability
זו גישה הפוכה לגמרי שאומרת – אל תשנה משתנים. כל ערך שתיצור יהיה immutable (כמו String), כל פעם שתצטרך ערך אחר – צור אובייקט חדש. גישה זו דיי קשה ולא כ”כ נתמכת בג’אווה. אם פיספסתי – הקומפיילר לא יתריע וגם יש כמה מצבים בעייתים שאצטרך להתחכם בהם. Closure דוגמא לשפה שתומכת בגישה זו באופן מובנה.

actors
זו גישה שפופולארית היום ב scala (השפה שנוצרה ל multi-core ו scalability). הגישה מדברת על active objects או actors שהם אובייקטים “חיים” – כלומר ה threads. לכל actor יש תור נכנס (“דואר נכנס”) והם מתקשרים אחד עם השני רק בהודעות אסינכרוניות (שמונעות מצב של deadlock). בזכות התקשורת – אין צורך בסנכרון בכלל. גישה זו ניראית מוצלחת וישימה, לא ניסיתי בעצמי – אך שמעתי כמה סיפורי הצלחה ממקור ראשון. נראה שהיא גם עובדת יפה כשהמערכת גדלה והופכת למסובכת.
אמנם בג’אווה אין תמיכה בשפה ל actors אך יש מספר ספריות (כגון akka או GPars) שלמרות שנכתבו בשפות שונות על ה JVM – עובדות יפה מתוך Java. הגישה מתאימה, אגב, גם לסנכרון בתוך אותו JVM וגם ב remoting בין JVMs שונים (כגון nodes שונים ב cluster).