התיאוריה המאוחדת: קוד, תכנון וארכיטקטורה

ניתן לומר שהגדרת ארכיטקטורה מורכבת מ 4 פעולות בסיסיות:

  • חלוקת המערכת למודולים / תתי מערכות
  • ניהול תלויות (בין המודולים)
  • יצירת הפשטות (Abstractions) והגדרת API
  • תיעוד הארכיטקטורה.
כמעט כל העקרונות והטכניקות של הגדרת ארכיטקטורה (למשל Quality Attributes או חלוקה ל Views) הן הנחיות כיצד לבצע פעולות בסיסיות אלו בצורה נכונה יותר."ניתוח Quality Attributes" היא טכניקה שכתבתי עליה בפוסט הזה והזה.

תכנון מונחה אובייקטים – OOD

תחום ה Object Oriented Design הוא תולדה של רעיונות שהתפרסמו במספר ספרים / מאמרים משפיעים – אך בניגוד למה שניתן לחשוב, אין הגדרה "חד-משמעית וברורה" מהם "עקרונות ה Object Oriented Design".
2 המודלים המקובלים ביותר להגדרת OOD כיום הם:

חוצמזה ניתן בהחלט להזכיר את תנועת ה Patterns (קרי POSA, PLOP, GOF) שלא ניסחה חוקים אלא "תיעדה" תבניות עיצוב מוצלחות – אבל יש לה השפעה ניכרת על הדרך בה אנו עושים היום Design (ולא תמיד לטובה).

העשור האחרון

זרם ה Agile השפיע גם הוא רבות על OOD וקידם כמה רעיונות:
  • "כשאתה מקודד – אתה בעצם עושה Design" (מקור: TDD) –> ומכאן רעיונות כמו "Design by Tests/Coding"
  • ההכרה שביצוע Design או הגדרת ארכיטקטורה הם Waste – שיש לנסות ולייעל אותם ("Just Enough Software Architecture")
  • ההבנה שחיזוי העתיד הוא דבר בלתי-מציאותי, גם על ידי אנשים נבונים למדי, במיוחד במוצרים חדשים אך גם במוצרים קיימים. ירידת קרנם של "העיקרון הפתוח-סגור" (מתוך SOLID) ו "(Predictable Variations (PVs" (מתוך GRASP) והצבת סימני שאלה בפני כמה מהעקרונות האחרים…

התאוריה המאוחדת

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

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

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

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

אפשר לראות שמות שונים ("גבוהים" ו"נמוכים") לרעיונות דומים – אבל ההקבלה הרעיונית יפה למדי.

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

הנה שתי דוגמאות:

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

עקרון ארכיטקטוני: Interface Segregation Principle 
עקרון זה אומר שמודול לא אמור להיות תלוי ב interfaces רבים שאינם בשימוש. אם נוצר מצב כזה – יש לפצל את ה Interfaces כך שהמודול יהיה תלוי, עד כמה שאפשר, רק ב Interfaces שהוא משתמש בהם בפועל. העיקרון נכון מאוד גם למתודות בתוך interface יחיד (רמת ה Design) או לפרמטרים בחתימה של פונקציה בתוך הקוד (רמת הקוד).

עוד היבט קוד שאני מאמץ מעקרון ה ISP הוא לנסות ולהימנע משימוש בספריות (כגון "Open Source") שיש לי מעט שימוש בהן. אני אשתדל לא להכליל במערכת ספרייה של אלף שורות קוד – אם אני משתמש רק ב 50 מהן. אני מעדיף למצוא ספרייה אחרת או אפילו לכתוב אותן שורות קוד לבד. מדוע? א. סיכוי לבעיות מ 950 שורות קוד לא רלוונטיות, ב. מסר לא ברור האם נכון "להשתדל" להשתמש במתודות אחרות בספריה או לא. ג. אם צריך לשנות / לדבג – ייתכן וצריך להבין הרבה קוד בדרך שלא רלוונטי למקרה שלנו.

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

  • מקבילה ל"ניהול תלויות" ברמת הקוד – לא מצאתי בדיוק. הסתפקתי בעקרון של הימנעות ממשתנים גלובליים.
  • לרעיון של חלוקת מודולים ע"פ "Unit Of Work" (כך שקבוצות פיתוח שונות יוכלו לעבוד במקביל עם מינימום תלות) – אני לא חושב שיש הקבלה אמיתית ברמת קוד.
  • העיקרון האלמותי של (DRY (Do Not Repeat Yourself הוא "No Brainer" בקוד, אבל הופך לנושא מורכב ולא חד-משמעי ברמת הארכיטקטורה.

בסופו של דבר – יש הרבה מאוד חפיפה בין עקרונות "ארכיטקטורה" לעקרונות "קוד", כך שאין כ"כ חדש ללמוד. הרעיונות חוזרים על עצמם בשינוי אדרת. חלק מהעקרונות (למשל KISS = Keep It Simple Stupid) הם פשוט אוניברסליים.

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

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

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

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

קושי מסוים בשימוש ב SLAP הוא שאין "מד רמת-הפשטה", כזה שנכוון אותו לשורה בקוד והוא יגיד לנו "רמה 6!" או "רמה 7!". זה הכל בראש שלנו כמפתחים ובני-אדם אינטליגנטים. לפעמים יהיו "סתירות" כך שיוחלט שפעולה X תהיה פעם אחת ברמה n ופעם אחרת ברמה n+1. אני אומר: זה אנושי. זהו עקרון חשוב – פשוט קחו אותו בפרופורציה ("We figured they were more actual guidelines").

סיכום

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

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

פ.ס. : הערות, מחשבות, ביקורות – יתקבלו בהחלט בשמחה!

שאלות על 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".

4 כללים למדידת פשטות של תוכנה

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

אחת ההגדרות שאני אוהב היא של בחור בשם J.B. Rainsberger, הגדרה של ״מבנה פשוט של תוכנה״.

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

ע״פ ריינסברגר (עוד), ישנם ארבעה אלמנטים (עם סדר עדיפויות ברור) המובילים לתוכנה פשוטה:

  1. כל הבדיקות עוברות. 
  2. צמצום כפילויות קוד. 
  3. הקוד מתאר כוונה (clarity). 
  4. צמצום  מספר האלמנטים בקוד למינימום האפשרי  אלמנטים שאינם משרתים את מטרות 1-3.

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

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

הנה דוגמה של קוד \"ללא כפילויות\" שהפריע לי והפכתי לכפול:

אני מסכים, זה לא קוד \"מושלם\", וספריית templating הייתה יכולה להפוך אותו ליפה יותר, אך זו דוגמה אמיתית מהחיים.

הנה הקוד לאחר השינוי:

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

קוד המתאר כוונה
אם מזקקים את הכלל של \"כתיבת קוד המתאר כוונה\", לרוב עיקר העבודה היא מסביב לשמות: שמות של פונקציות או משתנים, או שמות חדשים הנוספים ע\"י פעולות \"extract method\" או \"introduce variable\".

ישנן 4 \"דרגות\" של שם:

  1. שם סתמי
  2. שם נכון
  3. שם מדויק
  4. שם בעל משמעות (\"meaningful\").
עצלנות ולחץ גורמים לנו להיצמד לתחתית הסקאלה (1,2), בעוד הקפדה ומקצועיות דוחפים אותנו לראש הסקאלה (3,4).
מאוד נהניתי, כשקראתי את ריינסברגר, כשהוא מתאר בדיוק רב הרגל שגם אני רכשתי: כאשר אני רואה קוד כפול אני מבצע extract method לשורות הכפולות, גם מבלי להבין לעומק מה המשמעות שלהן. פעולה טכנית גרידא.
אני נותן לפונקציה שנוצרה את השם \"foo\". מבלי לחשוב. תוך כדי עבודה אני מבין מה הפונקציה עושה ומשנה את שמה. לאחר 10 דקות עבודה ייתכן והשם שונה כבר שלוש או ארבע פעמים, אך אני מרגיש בבירור שאלו עליות דרגה ברמה של השם. לעתים פשוט צריך לנסות איזה שם ו\"לחיות\" איתו כמה דקות על מנת למצוא שם מוצלח יותר.
צמצום אלמנטים שאינם משרתים את מטרות 1-3. 
למי שנתקל ברעיונות אג\'יליים – אני מניח שעקרון זה הוא מובן מאליו: Eliminate Waste. עדכון המטרה בעיקרון זה היא להימנע ממרכיבים בקוד שלא משרתים את הפונקציונליות, לא מונעים קוד-כפול ולא מסבירים את התנהגות המערכת, כל מיני \"הכנות למזגן\"[ג].

מי שעובד ב (Test Driven Development (TDD נהנה באופן מובנה מעקרון 1 (\"כל הבדיקות עוברות\") ועקרון 4 (\"צמצום אלמנטים לא-חיוניים\"). זו הדרך המהירה ליצירת קוד פשוט, שגם יישאר פשוט לאורך זמן.
מכאן נותרו רק 2 פעולות לשים לב אליהן:
צמצום כפילויות קוד [ב] ו כתיבת קוד המתאר כוונה / קוד ברור. המשיכו לעשות את אלו בצורה תמידית – והמבנה של הקוד שלכם, או ה \"design\" של הקוד שלכם – יהיה פשוט. זו הדרך היעילה ביותר שאני מכיר.
זה אולי נשמע קצת פשוט מדי: אולי ציפיתם לשימוש במחשבון של Cyclomatic complexity וטכניקות של ספירת Weighted Micro Function Points (ממש כמו ספירת קלוריות). 
צר לי לאכזב אתכם: כמה אנשים טובים השקיעו שנים ביצירת מודלים מתמטיים לתיאור סיבוכיות של תוכנה – אך בפועל כמה עקרונות פשוטים הם אלו שיגרמו לקוד שלכם להיות פשוט יותר, וקל יותר לתחזוקה.

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

—-

[א] יש המשתמשים במילה Design על מנת לתאר מצב של תוכנה חיה, ולא רק את התוכניות. לדוגמה: \"Refactoring: Improving the Design of Existing Code\". אם הרעיון הזה ברור לכם – הרגישו חופשיים להשתמש במילה design ולא ב structure.

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

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