2019-11-30

ללמוד מהקוד

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

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

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

את קטע הקוד הבא מצאתי בתשובה של StackOverflow, תשובה שזכתה לכמה upvotes [א].

אז בואו נתחיל.


הקוד


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

fun balance(amount: Int, round: Boolean, rate: Double, year: Int) : Double {
  var yearlyInterestPaid: Double
  var totalAmount = amount.toDouble()
  for (i in 0..year) { // for (int i = 0; i <= year; i++ ){
      yearlyInterestPaid = totalAmount * rate
      if (round) yearlyInterestPaid = Math.floor(yearlyInterestPaid * 100) / 100 .
      totalAmount += yearlyInterestPaid
  }
  return totalAmount
}

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



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

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

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

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

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


מבט שני


נחזור ונתבונן בקוד, לאחר שהסרנו את הבעיה הבולטת:


fun calcInterest(amount: Int, round: Boolean, rate: Double, year: Int) : Double {
  var yearlyInterestPaid: Double
  var totalAmount = amount.toDouble()
  for (i in 0..year) {
      yearlyInterestPaid = totalAmount * rate
      if (round) yearlyInterestPaid = Math.floor(yearlyInterestPaid * 100) / 100 .
      totalAmount += yearlyInterestPaid

  }
  return totalAmount}

האם אתם מזהים עכשיו בעיות נוספות?


----


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

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

fun calcInterest(amount: Double,
                round: Boolean,
                rate: Double,
                year: Int) : Double {
 
  var totalAmount = amount
 
  for (i in 0..year) {
      var yearlyInterestPaid = totalAmount * rate
      if (round) yearlyInterestPaid = Math.floor(yearlyInterestPaid * 100) / 100 .
      totalAmount += yearlyInterestPaid

  }
 
  return totalAmount
}

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

מה עוד? השארנו את הדברים הכבדים לסוף.

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

ובכן, כיצד נראית קריאה לפונקציה שלנו?

calcInterest(1000.0, true, 0.04, 10);

מה ניתן להבין מהשורה הזו?

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

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

יש IDEs שיוספו כ hint את שמות המשתנים ליד כל ערך - אבל שימו לב ששמות המשתנים לא פותרים את הבעיה:

calcInterest(amount: 1000.0, round: true, rate: 0.04, year: 10);


אם בכדי להבין מה שורה עושה, עלינו להיכנס לקוד הפונקציה - זה לא מצב טוב. בזבזנו זמן, וזו אינדיקציה חזקה מאוד שה "UX של הפונקציה״ (אפשר לקרוא לזה DX = Developer eXperience) - אינה טובה.

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



שיפור ה DX של הפונקציה שלנו


בואו נהפוך את חתימת הפונקציה שלנו לקריאה יותר, וכך - למובנת יותר.

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

כלל אצבע מוכר, הוא לא לשלוח יותר מ 4 פרמטרים לפונקציה. לעתים אנשים חושבים שהבעיה היא חתימה ארוכה של הפונקציה - ואז מעבירים את 12 הפרמטרים ל Request Object אחר בן 12 פרמטרים.

בעצם - מה ההיגיון בזה?!

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

val totalSalary = managersSalary + employeeSalary

עכשיו הופך ל:


val totalSalary = request.managersSalary + request.employeeSalary

זהו עומס נוסף על הקוד, שלא הופך אותו קל יותר לקריאה.

במקום ליצור Request Object אחד, עדיף לייצר 3-4 Request Objects קטנים המקבצים פרמטרים דומים יחדיו.

כך הגישה ל request object היא לא מעמיסה - אלה גם נותנת משמעות לפרמטרים. למשל:

val totalSalary = salaries.managers + salaries.employee

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

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

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

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


למי שעובד ב IntelliJ, הקיצור CMD+F6 (במק) - הוא קיצור חשוב מאוד. כשאתם מזהים סדר לא נהיר של פרמטרים - הקדישו חצי-דקה וסדרו אותם. IDE מודרני באמת מאפשר לבצע שינוי כזה במהירות ובבטיחות.

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


fun calcInterest(initialAmount: Double,
                interestRate: Double,
                years: Int,
                roundInterestYearly: Boolean): Double {

  var totalAmount = initialAmount

  for (i in 0..years) {
      var yearlyInterestPaid = totalAmount * interestRate
      if (roundInterestYearly) yearlyInterestPaid = floor(yearlyInterestPaid * 100) / 100
      totalAmount += yearlyInterestPaid
  }
  return totalAmount
}

שינוי פעוט בשם פרמטר, מ year ל years - הוא משמעותי.

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

ההעברה של round לסוף רשימת הפרמטרים מבליטה שהוא:
  • פחות חשוב מהפרמטרים האחרים (החישוב העיקרי נעשה בלעדיו)
  • קצת פחות קשור לפרמטרים האחרים (שהם הקלט המספרי לנוסחה)



לא סיימנו!


הקוד, אם קיבלנו אותו ל Review לא כולל חלק חיוני - בדיקות יחידה.

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

שאלות מהותיות הן:
  • האם אתם מאשרים PR כאשר פונקציה עם Pure Business Logic מוצגת - ללא בדיקת יחידה?
  • האם אתם מאשרים PR אם קוד לוגי הוא קרוב להיות טהור, ובמעט עבודה אפשר לזקק אותו - ולבדוק אותו בבדיקות יחידה?

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

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

לא כתבתי בדיקות יחידה לקוד (אני לרוב כותב, אם אני מפרסם קוד ב SO - כך צריך), אבל מהרצה פשוטה של הקוד ב IDE זיהיתי כבר שני מקרים שעלולים להיות בעייתיים, ובלתי צפויים:
  • הפעלת הפונקציה עם סכום של 1000, בריבית הנקובה למעלה, לאורך עשר שנים - מחזירה תוצאה של 1539.4540563150777.
    • כאשר אנו מבצעים עיגול כל שנה - הערך הוא עגול (1539.41), אך בהרצה לשש שנים - הערך המתקבל עם הרצת עיגול הוא 1315.9099999999999.
  • סכום חיובי וסכום שלילי - לא מתעגלים בצורה סימטרית (כי ה floor הוא אבסולוטי לערך).

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

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


אם אתם מופתעים, ושואלים את עצמכם ״אבל... מדובר ב Stack Overflow - אתר עם ביקורת רבה וקשוחה, איך זה יכול להיות?!״ אני אתן את הציטוט הבא שקראתי בו לאחרונה:
The research echoes an academic paper from 2017 that found 1,161 insecure code snippets posted on Stack Overflow had been copied and pasted into 1.3m Android applications available on Google Play. -- source

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

לא פעם יצא לי למצוא קוד מורכב / מאופטם בצורה חריגה לסביבה שלו - וחיפוש קצר בגוגל הראה שהוא הועתק, as-is, מ StackOverflow. אנחנו עושים את זה, ולא מעט. כולנו במידה כזו או אחרת ״Google Engineers״ (= כאלו שמחפשים בגוגל ואז מעתיקים את התשובה) - אבל חשוב לבדוק את הקוד ולא לסמוך עליו בעיניים עצומות.



סיכום


אשמח לדעת מה דעתכם.

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

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



----

[א] ליתר דיוק: שילבתי שתי תשובות שונות שזכו ל upvotes - על מנת לחדד כמה נקודות לדיון.



2019-11-17

Refactoring 2020

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

הספר Refactoring טבע כמה מושגי יסוד שהושרשו עמוק בתעשייה כמו Refactoring ,Code smells, או Testability של קוד. החלק החשוב בספר הוא לא קטלוג ה Refactoring (הטכני), אלא 4 הפרקים הראשונים העוסקים בכמה רעיונות מאוד חשובים בפיתוח תוכנה:
  • בדיקות-יחידה (Unit-Tests), היו אז עוד רעיון חדשני ולא כל-כך מוכר. 
    • שווה לציין שיותר ממה שבדיקות יחידה פותרים באגים, הם מאפשרים לעשות Refactoring במהירות ובביטחון.
  • הרעיון שהשלב הראשון בפתירת באג, היא לכתוב בדיקה שתיכשל - ואז לגרום לבדיקה לעבור (מה שיוכיח שהבאג תוקן, ויוודא שאין רגרסיה והוא חוזר). 
  • הרעיון שניתן לשנות/לשפר Design של מערכת קיימת אחרי שהקוד נכתב. 
    • הרעיון הזה תומך ומחזק רעיונות כמו Minimum Viable Products (בקיצור: MVP) ו YAGNI - רעיונות סופר-חשובים ומשמעותיים.
    • הרעיון הזה, לצערי, לא חלחל לכל קצוות התעשייה עד היום. 
  • הרעיון ש Refactoring צריך להיות חלק מובנה מתהליך פיתוח תוכנה, כמו ש Delivery הוא כזה. כמובן שבלי delivery אין פיצ׳רים ללקוחות, בעוד refactoring ניתן לדחות ולדחות עד ה Rewrite הבלתי נמנע.
    • בספר פאוולר מקדיש חלק ל״איך לשכנע את המנהל״ לבצע Refactoring. עצה אחת היא שהמנהל לא יאהב את רעיון ה Refactoring, אבל אם האלטרנטיבה היא Rewrite - אז הוא יעדיף אותה. עצה שניה היא פשוט לא לספר למנהל - אלא פשוט לעשות. הדילמה הזו רלוונטית גם היום.




האם רעיון ה Refactoring באמת הצליח?


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


האם באמת אנו פועלים כך ש Refactoring הוא חלק בסיסי מהעבודה?

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

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

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


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

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

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


Software Death Spiral
ביצוע Refactoring אמור להחזיר אותנו מעט לאחור - ולהאריך את קץ המערכת...



האם חשוב לעשות Refactoring?

האם חשוב לעירייה לפתח את העיר עליה היא אמונה? לסלול כבישים, לשנות ייעוד של מבנים, ולשנות חוקים בתוכנית הבניה העירונית (תב״ע)?

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

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

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

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

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

בכל זאת Refactoring הוא תהליך של הארכת חיי המערכת - והוא תהליך חיוני לארגונים שרוצים להיות יעילים.



אז למה לא לעשות Refactoring?

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

למה?

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

״תנו לנו 3 שבועות, נבצע Refactoring באזור ההוא - ואז הכנסה של פיצ׳רים תהיה קלה בהרבה!״

אבל המנהלים, שהם אמונים על ובקיאים בתמונת הפרויקט, רואים לעתים ש:
  • ה Refactoring קצת הסתבך וארך בעצם כמעט 5 שבועות.
  • פיצ`רים לא תמיד יותר מהירים לאחר ה Refactoring, לפחות לא משמעותית: ״כן, ה Refactoring שיפר מהירות של פיצ`רים מסוג z, אבל זה פי׳צר מסוג y...״
  • ה Refactoring עלול לגרום לרגרסיות במערכת ו/או באגים נוספים.
באמת, ממעוף הציפור, ומבלי להכיר את הפרטים - Refactoring אכן עשוי להישמע כדבר שרצוי להימנע ממנו, ולדחות אותו.


מי צודק? המנהלים או אנשי-התוכנה?



למה Refactoring נכשלים?


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

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

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

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



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

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

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


יאללה, המלצות (סיכום)


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

האם מפסיקים לעשות Refactoring ומתכננים כבר ב Schedule מועד ל Rewrite לכל חלקי המערכת?
האם פשוט צריך להחליף את ההנהלה? להחליט שימי ראשון מוקדשים ל Refactoring בלבד?

הנה כמה המלצות קונקרטיות, ממיטב ניסיוני:
  • Refactoring גדול = סיכון גדול, Refactoring קטן = סיכון קטן. 
    • דוגמה קלאסית ל Refactoring עם ROI חיובי ברור הוא Rename לאובייקט מרכזי במערכת שהשם שלו מטעה / פשוט לא נכון. ההשקעה היא במסגרת הדקות - התמורה היא ודאית.
    • נסו ליצור תרבות של Refactoring קטנים עליהם לא צריך לספר למנהל (כפי שהציע פאוולר), ולא צריך לשנות בשבילם Schedule של פיתוח. סיכוי טוב שה ROIs יהיה מוצלח, וגם תהליכי ה Code Review יוכלו לספק בקרה ופידבק לגבי הנכונות ההנדסית של ה Refactoring הללו.
  • כאשר אתם עושים Refactoring גדול, התייחסו אליו כמו כל פיצ׳ר גדול ומסוכן אחר:
    • עשו בקרה לעיצוב המוצע. למשל: Design Review.
    • נסו ברצינות לפרק אותו לשלבים קטנים יותר / לבצע MVP שיספק פידבק מוקדם יותר עד כמה השינוי מוצלח/יעיל.
    • נסו לצמצם אותו ולחשוב: מה אפשר להוריד מהמודל היפה והאלגנטי הזה שמוצע. על אלו פרטים ניתן לוותר - בלי לאבד הרבה מהערך. מניסיון - זה עובד.
  • הכי מסוכן זה Refactoring גדול של מישהו שקיבל לאחרונה אחריות על קוד. למשל: חודש-חודשיים אחרי שצוות מקבל קוד מצוות אחר, הוא ״מיואש״ כבר מהקוד - ודורש לבצע Refactoring גדול.
    • יש בכך יתרון: תוך Refactoring כזה, הקוד בהדרגה "יהפוך לשל הצוות": הוא יהיה מוכר יותר והצוות ירגיש שלם יותר איתו. Refactoring היא דרך מעולה להבין קוד לעומק.
    • הסיכון הגדול הוא שהצוות לא מבין את הדומיין טוב מספיק עדיין, וסיכון גדול שהוא יבצע שינוי ל Design שאינו טוב יותר.
    • נסו למנוע / לצמצם עבודת Refactoring גדולה של מישהו שלא עבד עם הקוד חצי שנה לפחות. כן עודדו לעשות כמה שיותר Refactoring קטנים (כמה שעות כ״א, למשל). זה כלל האצבע שלי.
  • אל תוותרו על Refactoring, גם אם זה קצת מורכב:
    • Refactoring הוא אוויר לנשימה למערכת. מערכות דורשות חמצן והתחדשות - ותהליך ה Refactoring מספק להן את זה.
    • לא פחות חשוב: Refactoring הוא אויר לנשימה למהנדסים שעובדים על המערכת. 
      • זה ממש רע לקבל, ולהתרגל לעיוותים וחוסרי-הגיון בקוד - זו דרך טובה להפוך למהנדסים פחות טובים.
      • Refactoring הוא גם רענון שכלי למפתח (שאנו לא רוצים ״שיתייבש״): עבודה הנדסית שקצת שונה לפעמים מבוספת פיצ'רים למערכת.
      • Refactoring עוזר לקבל רמת עומק גבוהה יותר עם הקוד שמתחזקים, במיוחד לאורך זמן (״הנה המשמעות של מצב A, מול המשמעות של מצב B״)


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