2013-08-28

סיפור: שבעה קווים אדומים

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

----------------------------------------------------------
המשתתפים:
אורן - חפ"ש ("חייל פשוט") , ומנהלו - מוטי.
שושן - חפ"ש ממחלקת השיווק, והמנהלת שלו - עופרה.
----------------------------------------------------------

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

"יכולים!" ענה מוטי - ללא היסוס. מוטי לא נהג לוותר על משימות שהציבו בפניו (או בעצם: בפני הצוות שלו).

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

"לא", ענה אורן באופן מיידי.

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

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

"אורן - למה אתה מתכוון 'לא אפשרי'? " הגביה מוטי מעט את הטון.

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

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

"אפשר לצייר קו אדום עם צבע אדום, וקו ירוק עם צבע ירוק - אבל לא לשלב ביניהם...".

"שנייה! אל תבלבל אותנו. לפני שנייה אמרת שזה אפשרי!" נכנס מוטי לדבריו.

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

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

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

אורן הניד בראשו: "צבעים של אור ניתן אולי להפריד, זה לא רלוונטי לצבעים שצוירו על דף".



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

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

"הממ, איך אני אסביר זאת... אתה יודע מה זה 'שקוף', נכון?"

"בוודאי"

"ואני מאמינה שאני לא צריכה להסביר מהו קו אדום?"

"לא. אני יודע היטב".

"יופי! פשוט תצייר לנו קווים אדומים בצבע שקוף".

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

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



"טוב, בואו נשים את עניין הצבע לרגע בצד" הציע אורן "אמרתם שאתם רוצים קווים ניצבים?"

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

"ניצבים למה?" שאל אורן.

"א.. נו.. מממ... אני לא יודעת בדיוק. אתה המומחה, חשבתי שאתה תדע למה הם ניצבים..."

"ברור שהוא יודע!" עודד אותו מוטי מהצד, "זו המומחיות שלנו, או לא?"

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

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

"אני מסכימה!" אמרה עופרה.

אורן הלך לצד החדר, עצר לרגע, וחזר כשבידיו דף לבן ועט.

"בואו אני אראה לכם... הנה קו ישר, בסדר?" עופרה הנהנה בראשה "והנה קו שני ישר שניצב לקו הראשון".

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

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

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

"אני יכולה לנסות בעצמי?" שאלה עופרה.

אורן מסר לה את העט. עופרה ציירה כמה קווים בזהירות "ומה עם זה?", שאלה.

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

הפרצוף של עופרה הראה סימני ייאוש.

"אבל למה הם כחולים?" שאל מוטי.

"כן, למה הקווים שציירת כחולים?" שאלה עופרע.

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

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

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

"למה לא הכנת עט אדום מראש?" נזף בו מוטי "ידעת על הישיבה הזו מיום שני!"

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

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

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



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

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

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

"נכון מאוד!" ענה מוטי לפני שאורן פתח את פיו.

"לא... זה לא מה שאמרתי" אורן לא התאפק ואמר.

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

"נכון."

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

"כן... טוב, נכון."

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

אורן התבונן בשושן בסלחנות.

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

"בדיוק, מושלם! אז בואו נתחיל מיד בעבודה! חבר'ה, יש עוד שאלות?"

"הו!", נזכר שושן, "יש את הבלון האדום הקטן. אתה יכול למלא אותו באוויר בשבילנו?"

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

"אורן", פנה אליו מוטי בחיוך, "תסגור לנו גם את הפינה הזו?"

"אבל איך הבלון קשור אלי?!" שאל אורן בשקט. לפתע עצר כשאצבעותיו החלו רועדות...

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

"הממ...", אורן ענה בזהירות רבה, "בעיקרון כן, אבל..."

"נהדר!" סיכם מוטי את העניין.

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

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

"בוודאי", ענה אורן, "אני יכול לעשות הכל. אני הרי מקצוען."


----

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

תודה לדימה ששלח לי את הלינק!




2013-08-22

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

תזכורת קצרה:

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

תהליך הגדרת הארכיטקטורה מורכב מ 4 פעולות עיקריות:
  1. חלוקת המערכת ליחידות קטנות יותר ("מודולים")
  2. ניהול תלויות
  3. הגדרת הפשטות (abstractions) והגדרת API
  4. תיעוד הארכיטקטורה
כמובן שהתהליך לא חייב להיות מנוהל בנוסח "מפל המים" (קרי - כל הגדרת הארכיטקטורה לפני כתיבת הקוד), אלא זה יכול להיות תהליך איטרטיבי שמתרחש תוך כדי כתיבת קוד.

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

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



חלקים של מערכת. כיצד נקבעה החלוקה?


מדוע לחלק מערכת למודולים?

חלוקת המערכת למודולים מסייעת להתמודד עם מספר בעיות (מתוך הפוסט "מדוע אנו זקוקים ל Software Design"):
  • בעיית ההתמצאות (במידה רבה)
  • בעיית ההקשר / הגבולות הברורים (במידה רבה)
  • בעיית מקביליות הפיתוח
  • בעיות הדומינו (במידה מסוימת)
  • בעיית ה Deployment (במידה מסוימת)
  • שימוש חוזר בקוד (במידה מסוימת)
קריטריון מרכזי לחלוקת המערכת למודולים היא יצרת "יחידות עבודה" ("Units of Work") עליהם יוכלו לעבוד מפתחים שונים במקביל (מה שמתחבר באופן ישיר לבעיית מקביליות הפיתוח). חלוקה למודולים בצורה הגיונית תסייע מאוד לבעיית ההתמצאות ובעיית ההקשר.

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

דויד לורג' פרנס תיאר זאת כך: "מתכנת חדש לא צריך ללמוד את כל המערכת בכדי לשנות שורת קוד אחת"


אז איך מתחילים?

בשלב הבסיסי ביותר ננסה לטפל בבעיות ההתמצאות ובעיית ההקשר / הגבולות הברורים. אנו רוצים שמתכנת שצריך לבצע שינוי ייגש לתרשים המערכת וממנו יבין את האזור (מודול) בו השינוי צריך לקרות. לשם כך אנו נחלק אחריויות כך ש:
  • לכל מודול תהיה אחריות יחידה
  • כל אחריות תהיה שייכת למודול אחד בלבד.
כלל זה בעצם הוא כלל ה Single Responsibility Principle (בקיצור: SRP), הכלל הראשון במערכת חוקי ה SOLID.

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

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

ביטוי אחר מפורסם של כלל ה SRP ידוע כעקרון ה Separation of Concerns, כלל שאומר ש:
"System elements should have exclusivity and singularity of purpose", קרי - אותו הדבר, במלים גבוהות. זהו התיאור האהוב על ארכיטקטים רבים.

כלל ה SRP הוגדר ע"י דוד בוב בצורה הבאה: "A class should have one, and only one, reason to change". זו לא הדרך הכי פשוטה לבטא את הכלל, בהמשך נסביר את ההיגיון מאחורי הגדרה זו.

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


ובכן, נראה שהכלל מאוד הגיוני, אך הוא מעט סתום ללא הסבר נוסף.



מהי אחריות (Responsibility)?

קרייג לרמן הטיב לתאר[א] את מהות האחריות: אחריות היא מומחיות, ידע שרק המחלקה / המודול הספציפי יודע:
  • היכרות אינטימית עם מבנה נתונים מורכב
  • ביצוע חישוב מסוים (למשל חישוב ריבית או זמן נסיעה)
  • היכרות עם פורמט (עבור Parser) או פרוטוקול (למשל HTTP או פרוטוקול אפליקטיבי)
  • היכרות עם Flow (סדר פעולות ב high level) במערכת
שינוי יחיד בדרישות של המערכת מתמפה לעתים קרובות ל"מומחיות" יחידה של המערכת. אם המומחיות מוכמסת (encapsulated) במחלקה יחידה - אזי השינוי יהיה נקודתי.

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


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

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

יש לי 3 הערות על הגדרה זו:
  • זו לא הגדרה כ"כ ברורה (לדעתי)
  • הגישה של כתיבת מחלקות "שלא ייגעו בהן יותר" קצת השתנתה בשנים האחרונות. נדון בהרחבה בנקודה זו בסקירת "העיקרון הפתוח-סגור" (OCP).
  • נקודה שאני אוהב בהגדרה זו היא ההצמדה של האחריות - להבנה סובייקטיבית שלנו של המערכת ("מה צפויים להיות השינויים"). בכלל ב Design אין "מתמטיקה" שמובילה אותנו לפתרונות טובים. החלטות ה Design מבוססות על הבנת הבעיה / מערכת + הגיון בריא (בשאיפה).
    ישנם גם Guidelines (להלן העקרונות להנדסת תוכנה) המסייעים לנו להגיע להחלטות הנכונות.

שמירה על שני האלמנטים של עקרון ה SRP הוא תנאי מקדים למודולים שיוכלו לשמש במערכות אחרות (להלן Code Reusability).


חזרה לדוגמה לעיל:


אם נתבונן בחלוקה לתתי-המערכות של הדפדפן, נוכל לראות שיש כאן חלוקה ברורה למומחיות:
מודולים 1-3, 5 - התמחות בתחום מסוים ומוגדר היטב
מודול 4 הוא יותר אינטגרטיבי ומשתמש במודולים 1-3,5 לביצוע משימותיו.
מודול 6 מומחה בניהול Flows.
מודול 7 ומודול 4 מחלקים ביניהם את המסך: מודול 7 הוא המסגרת ומודול 4 הוא מה שבאמצע.

מה שמוביל אותנו ל...


עקרון ה Unit Of Work

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

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

שאלה נוספת: כיצד החליטו בוני הדפדפנים לחלק בין ה Shell ל Rendering Engine ל Graphics Module? הרי שלושתם "מומחים לציור על המסך"?

עקרון יחידות העבודה ("Units of Work" של פרנס, לא לבלבל עם תבנית העיצוב של פאוולר בשם זהה) מוסיף לפעולה של חלוקת המערכת למודולים הקשר חשוב: הצוות המבצע.
  • אם את פייסבוק היה כותב אדם אחד (צוקרברג?) - אזי מודול אחד הוא בסדר. אין התנגשויות.
  • אם את המערכת כותבים 2 צוותים של חמישה מתכנתים כל אחד - אזי מודול אחד הוא לא בסדר, מכיוון שתהיה התנגשות בעבודה.

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

כלומר: קודם יש לדאוג למקביליות הפיתוח ("Unit of Work") ורק לאחר מכן להתמצאות / קביעת גבולות ברורים. לרוב יש התאמה גבוהה (correlation) בין המשימות כך שלא תרגישו שאתם עובדים על דברים שונים.

נחזור לבעיית פייסבוק: מכיוון שיש לנו 2 צוותים וקשה להם לעבוד על מערכת כל-כך גדולה ללא חלוקה מסודרת - יש לחלק את המערכת למודולים. כאשר אנו מחלקים מערכת למודולים בחשיבה על צוות העבודה יש לקחת בחשבון גורמים כגון:
  • גודל ומורכבות המערכת - יותר גודל או מורכבות => יותר מודולים
  • רמת ואחידות הצוותים / המפתחים - ייתכן ונרצה לרכז מורכבויות או מומחיויות טכניות במודולים מסוימים
  • חלוקה לצוותים / מיקום גאוגרפי - כמה צוותים יש וכמה סביר שהתקשורת ביניהם תהיה טובה? כאשר התקשורת קשה (צוותים המופרדים ע"י מסדרון ארוך, אוקיינוס או סתם לא ביחסים טובים) כדאי לתכנן מודולים עצמיים שידרשו כמה שפחות תיאום בין הצוותים (קשור לנושא ניהול התלויות).
  • Legacy (שקשה לשנות) מול קוד חדש (שקל לשנות) - כדאי לקחת בחשבון ש Legacy Code [ד] קשה יותר לחלוקה, וכדאי להתגמש בנושא זה ולא לנסות לחלק אותו כמו שמחלקים קוד חדש.

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

זו מחשבה מקובלת, אבל בהחלט לא יעילה.

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

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

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



ארכיטקטורות אינטסנט

כדי להקל על המפתחים / ארכיטקטים, נוצרו מספר ״מתכונים מוכנים״ לחלוקת המערכת למודולים + ניהול התלויות.
חלוקה ע״פ שכבות - היא אולי המוכרת מכולן. ארכיטקטורות אינסטנט מוכרות אחרות הן MVC ו Pipes and Filters.



לחלוקה ע"פ שכבות יש מספר וריאציות:
  • n שכבות, כאשר כל שכבה יכולה לפנות רק לשכבה האחת שמתחתיה.
  • n שכבות, כאשר כל שכבה יכולה לפנות לכל שכבה שמתחתיה.
  • Shared Layers, בה יש שכבות "מאונכות" המשותפות לכמה שכבות.
  • 3 שכבות שנקבעו מראש: ממשק משתמש (UI), לוגיקה עסקית (Business Logic) ואחסון מידע (Persistence).
מכיוון שפוסט זה אינו עוסק בניהול תלויות, אדלג על ההבדלים בין 3 הווריאציות הראשונות ואתמקד רק בווריאציה האחרונה. מאחורי חלוקה זו ישנו רעיון דיי מתוחכם: חלוקת המערכת ע"פ טווח השרידות של חלקיה.

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

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

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

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


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

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

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


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

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

ניסינו להסביר שיש לנו חלוקה (ותלויות) מותאמים טוב יותר מ MVC [ה], שאנחנו מאוד בעד חלוקה מודולרית במערכת, ובעצם עשינו צעד אחד מעבר - אבל הדאגה בצד השני רק גברה. התוצאה הייתה מבלבלת: הצוותים ניסו להכניס MVC בצורות לא הגיוניות :"כל Service מחולק למודל ו Controller. ה View - ריק" או מיפוי לא נכון של הרכיבים ל MVC והסקת מסקנות לא נכונות לגבי השימוש בהם. במקרה קיצוני יצרו רכיב UI קטנטן שכלל MVC שלם ותקשר בהודעות עם עצמו.

עשו טובה: אל תגיעו למצב הזה.

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


סיכום ביניים

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

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

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




---

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

[ב] עקרון זה, ה Information Hiding, הוא עיקרון שהוגדר ע"י פרנס במאמר פורץ הדרך שלו On the criteria to be used in decomposing systems into modules. העיקרון נשמע אולי טריוויאלי היום אבל בזמנו הייתה עליו לא מעט ביקורת ("למה להחביא, מה אנחנו - קומוניסטים?").

ספר מפורסם למדי, The Mythical Man Month, תקף את הרעיון בגרסתו הראשונה (1975) ובגרסה מעודכנת של הספר שיצאה לאחר 20 שנה, כתב פרדריק ברוקס פרק שמתחיל במשפט: "...Parnas was right, and I was wrong".

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

[ד] ישנה הגדרה שאומרת ש Legacy Code הוא כל קוד שאינו מכוסה היטב בבדיקות יחידה.

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



2013-08-03

מדוע אנו זקוקים ל Software Design?

בפוסט זה אני רוצה לחזור לבסיס ולסקור מדוע אנו זקוקים לפעולת ה Software Design.

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

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

אם אתם תוהים על "חישובי עלות-תועלת ב Design" - קראו את הפוסט הקודם שלי בנושא.



להלן סדרת בעיות התוכנה, ש Design יכול למתן:



בעיית ההתמצאות (Orientation)

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

דרכים אלו גוזלות זמן ומהוות waste שנרצה להימנע ממנו.

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


בעיית ההקשר (Context) / הגבולות הברורים (Boundaries)
תת-בעיה של בעיית ההתמצאות.

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

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

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





בעיית "נגעת - שברת" (Software Fragility)

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

האם אפשר לתכן תוכנה אחרת, כך שהסיכוי "לגרום לשבירה" יפחת?

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



בעיית הדומינו (Software Rigidity)

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

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

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




בעיית ה Development Scalability

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

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

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

לבסוף, אנו רוצים שאינטגרציות יכללו מינימום "התנגשויות" - ומכאן יצטמצמו פעולות ה merge ברמת ה source control. בשביל ה productivity.




בעיית ה Deployment

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

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

לעדכון של מערכת גדולה יש עלות משמעותית: אפשר לעבוד ימים, אם לא שבועות בכדי לעדכן מערכת גדולה ומורכבת.
כאשר יש Persisted Data, שינוי במבני הנתונים יכול לדרוש תהליך של migration: תהליך שיכול לארוך שעות של downtime ולא-תמיד הוא נקי מתקלות.

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

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



בעיית הפריון (Productivity)

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

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

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



בעיית הסיבוכיות

זוהי מטא-בעיה שמסכמת את הבעיות שציינו:
  • בעיית ההתמצאות
  • בעיית ההקשר
  • "נגעת-שברת"
  • בעיית הדומינו
  • בעיית ה Development Scalability
  • בעיית ה Deployment

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


סיכום ביניים

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

על מנת שנוכל להתאים כלי של Design לבעיה, כדאי לרדת מרמת הדיון על "סיבוכיות" ולדון ברמת בעיות המדויקות יותר: development scalability, בעיית ההתמצאות או בעיית Deployment.

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


אני לא יכול שלא להזכיר בנושא זה סוג נוסף של "בעיה":



השאיפה לשימוש-חוזר (Reusability)

שימוש-חוזר (Reusability) הוא אחד הדברים שהכי קל "למכור" בצוות / בארגון: "למה לכתוב אותו הקוד פעמיים? בואו נכתוב פעם אחת ונשתמש בו בשני המקומות!"

יש לי הרבה מה לומר בנושא, אך אנסה לקצר בעזרת 2 חוקים בנושא, חוקי ה 3x3 של השימוש-החוזר:
  1. כתיבת רכיב המתאים לשימוש חוזר היא פי 3 יותר קשה מכתיבת רכיב לשימוש יחיד [א].
  2. רק לאחר שרכיב בשימוש ע"י 3 מערכות שונות - הוכחה היכולת שלו להתאים לשימוש חוזר.
אמנם יש מקרים בהם שימוש-חוזר הוא פשוט יותר, אבל לרוב מדובר במצב בו רכיב בודד צריך לטפל בכפליים תסריטים וכפליים מאפייני איכות - מאפיינים ברורים המגדילים את הסיבוכיות שלו. התסריטים ומאפייני האיכות הנוספים לא תמיד צצים בבחינה ראשונית של הנושא, במיוחד אם זו בחינה בגובה "10,000 רגל", שלא נכנסת לפרטים.

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

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



סיכום

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

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


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


---

[א] אין כמובן דרך למדוד "פי 3 יותר קשה", במיוחד כהכללה. הכוונה היא כמובן לומר: "הרבה יותר קשה".