הפרדת רשויות: מדוע להשקיע ב DTOs ו Entities?

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

תוכנה מתחילה למות ביום בו מפסיקים לשנות אותה.

עקרון תכנותיקה

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

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

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

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

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

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

מדוע לבצע הפרדת רשויות?

לפעמים יש לנו אובייקט מרכזי במערכת. למשל: Person. אנחנו משתמשים בו לשלושה שימושים עיקריים:

  • ב APIs או Events – כדי להעביר מידע אודות Person למערכות אחרות.
    • לפעמים ע״י serialization של המחלקה ל JSON
    • לפעמים אנחנו משתמשים במחלקה על מנת להפיק (generate) ייצוג ב IDL (שפת ביניים)
  • ב Business Logic – בכדי להעביר מידע אודות Person בין חלקים שונים של ה business logic.
  • בשכבת העבודה של בסיס הנתונים – בכדי לשמות מידע של Person בבסיס הנתונים לאורך זמן.
    • לפעמים ע״י serialization של המחלקה ל JSON (למשל: Document databases, או semi-document DB כמו mySQL או Postgres).
    • לפעמים דרך ORMs כמו Active Records או Hibernate (שיצרו סכמת בסיס נתונים בהתבסס על מבנה האובייקט).

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

  • האובייקטים, הרבה פעמים, יהיו זהים ביום היצירה שלהם? אז למה לשכפל קוד?
  • למה לתרגם בין אובייקטים זהים (נניח: ברגע שאוביקט ב Business Logic צריך לעזוב את המערכת כחלק מ Event / קריאת API)

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

Entity Object
(e.g. PersonEntity)
Model
(e.g. Person,
no suffix)
Data Transfer Object (e.g PersonDTO)
שמות מקובלים אחריםPersonDBEntity (clarify the direct relation to DB).PersonModel, PersonDomain, PersonBL (= Business Logic)None I know of.
סיבה להשתנותהוספת מידע שנדרש לצורך שמירה בלבד: ID, זמן / תאריך שמירה או שינוי. שדה נוסף שיקל על פעולת אינדוקס.
אולי רוצים לפצל את שמירת הנתונים ל-2 טבלאות או פורמט אחר לצורך שיפור ביצועים.
העשרת ה BL בשדות / תכונות נוספות שפנימיות למערכת.
שינוי שמות שדות בעקבות תובנות ואפשרות לתאר אותם בצורה נכונה יותר, ייצוג נתונים באופן שקל יותר למערכת לעבוד איתו. למשל LocalDate ולא מחרוזת של תאריך, Money ולא Integer.
התאמת מבנה נתונים ללקוח, נניח אפליקציות FE שייהנו ממבנים ידידותיים יותר ל JS.הוספת שדות מחושבים שלא נמצאים במודל, אך יקלו על הלקוחות (BE או FE).
כיצד משתנה בצורה תואמת-לאחור
(למשל הוספת שדה חדש שניתן לקבוע לו default value)
תאימות לאחור חשובה כי ייתכן ונרצה לקרוא מחר מידע שנשמר לפני שנה-שנתיים. ללא תאימות לאחור – לא נוכל לאחזר מידע ישן.גמישות רבה בשינויים, כי אובייקט המודל לא נשמר ולכן כל אתחול של המערכת (deploy) יכול לעבוד עם גרסה חדשה.תאימות לאחור חשובה כי ישנם לקוחות שימשיכו לצפות ולשדר את המבנה בגרסאות קודמות שלו – ואין לנו שליטה עליהם (אם הלקוחות הם שירותים שלנו – עדיין יש צורך בשינוי הדרגתי).
כיצד משתנה בצורה שאינה תואמת-לאחוראפשרות א: תיקון כל הנתונים בבסיס הנתונים (migration) כך שיתאים ל entity החדש. זה שינוי שיכול להיות קשה, יקר, ומועד-לטעויות יקרות. כיף!אפשרות ב: ליצור גרסאות של ייצוג בבסיס הנתונים, ולהחזיק קוד שמזהה את הגרסה – ויותר לטפל בכל גרסה באופן שונה.אפשרי ברמת הקוד בלבד (refactoring). כל עוד הקוד מתקמפל, והבדיקות עוברות – כנראה מאוד שאנחנו בסדר.לרוב נאלץ לפתור גרסה חדשה של ה API / event (למשל V2) בו יש את המבנה החדש, ולהעביר לקוחות לגרסה החדשה. עבור לקוחות שאין לנו שליטה עליהם – זה יכול להיות תהליך של חודשים הכולל פשרות מסוימות.
הערותלפעמים אנשים מבלבלים בין Entity ו DAO:
Entity – ה object שחוזר.
DAO – הממשק שממנו שולפים את ה Entity.
לא פעם מכיל מתודות / פונקציות – ולא רק נתונים.
מומלץ מאוד שאלו יהיו רק מתודות המקלות על גישה / פענוח הנתונים (מה שנקרא access logic), ולא Business Logic של ממש.
לא פעם מקובל להגדיר Coarse-grained DTO (אובייקט ״גדול״ יותר) – על מנת לצמצם את מספר הקריאות ברשת.
השוואה בין ההבדלים החשובים בין Entity, Model, ו DTO.

דוגמאת קוד

המודל:

@JsonIgnoreType
public class Person {
  @JsonIgnore public final String name;
  @JsonIgnore public final LocalDate birthDate;

  private static final DateTimeFormatter formatter = DateTimeFormatter.ofPattern(PersonDTO.birthDateFormat);

  public Person(String name, LocalDate birthDate) {
    this.name = name;
    this.birthDate = birthDate;
  }

  public PersonDTO toDTO() {
    return new PersonDTO(name, birthDate.format(formatter));
  }

  static public Person fromDTO(PersonDTO dto) {
    return new Person(dto.name, LocalDate.parse(dto.birthDate, formatter));
  }
}

הערות:

  • לא השתמשתי ב record, בהנחה שזה מבנה פחות מוכר, אז למה לבלבל.
    • שימוש ב public members עבור Entities – הוא דבר שעשיתי בתחילת שנות האלפיים, ותמיד האמנתי שהוא נכון (למרות מעט ביקורת מצד תאורטיקני ג׳אווה שתמיד רצו getters).
  • שורות 1, 3, ו4: JsonIgnoreType@ ו JsonIgnore@ הם ביטחונות של ספריית Jackson (ל seralize/deseralize JSON) שהמחלקה לא תסורייל (seralized) ל JSON ושהמידע שלה לא יישמר/ישלח איכשהו. אנשים נוטים לשכוח שיש DTO ו/או Entity – וחשוב להגן בפני הטעויות הללו. אם התחלנו לשמור את המודל לבסיס הנתונים – התיקון עלול להיות יקר.
    • האם שניהם נחוצים? (ולא מספיק אחד) – אני לא בטוח, אבל Better be safe than sorry.
    • לא כולם משתמשים ב Jackson כמובן – עליכם למצוא את הפתרונות שלכם להגן על המודל שלא ייצא מגבולות ה business Logic ולא יישמר באופן שיחסום אתכם לשינויים עתידיים.
    • האם יש משהו לא-אלגנטי שדווקא המחלקה שאמורה להיות ״הנקיה ביותר״ צריכה להשתמש בתלות לספריית ה serialization ולהצהיר – שהיא ״לא במשחק״? בהחלט לא אלגנטי – אם תמצאו פרונות אלגנטיים יותר אך מעשיים – לכו עליהם.
  • שורות 13 ו 17: אנו רוצים פונקציות עזר פשוטות בכדי להמיר בין מודל ו DTO.
    • לרוב קוד ה DTO יאוכסן בספריה נפרדת, כחלק מה API של המיקרו-שירות / המערכת – ולכן ל DTO לא תהיה גישה ל Model (והגיוני שכך). קוד ההמרה חייב לשבת במודל.

ה (Data Transfer Object (DTO:

public class PersonDTO {
  public final String name;
  public final String birthDate;

  @JsonIgnore public static final String birthDateFormat = "dd/MM/yyyy";

  public PersonDTO(String name, String birthDate) {
    this.name = name;
    this.birthDate = birthDate;
  }
}

הערות:

  • שורה 5 – הפורמט שבו ה DTO שומר את התאריך כמחרוזת (נניח: פורמט שקל לצרוך מתוך JavaScript) הוא פרט מומחיות שלו, ולכן יושב על ה DTO ולא על מחלקת המודל.
    • בהנחה שאנו עובדים עם Jackson – לא נרצה שפרט זה יעבור על הרשת כחלק מהאובייקט – ולכן השימוש ב JsonIgnore@.

ה Entity:

class PersonEntity {
  public final String name;
  public final String birthDate;

  private static final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");

  PersonEntity(String name, String birthDate) {
    this.name = name;
    this.birthDate = birthDate;
  }

  public Person toModel() {
    return new Person(name, LocalDate.parse(birthDate, formatter));
  }

  static public PersonEntity fromModel(Person model) {
    return new PersonEntity(model.name, model.birthDate.format(formatter));
  }

}

הערות:

  • שורות 12 ו 16 – ה Entity הוא זה שמכיר את המודל, כי נרצה שמי שיעשה את ההמרה הוא שכבת ה Data Access ולא ה business Logic. למשל: DAO או Repository המקבלים את המודל ושומרים אותו, או שולפים אובייקט מודל לפי שאילתה נתונה.
  • שורה 5 – הפורמט שבו אנחנו שומרים את התאריך (נניח: פורמט ידידותי ל DATE column בבסיס הנתונים) הוא מידע פרטי של ה Entity.
    • לא הוספתי JsonIgnore@ – כי זה שדה פרטי.

סיכום התלויות:

סיכום

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

שאלה גדולה היא מתי לעשות את זה?

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

ברור לי ששתי הקיצוניות הן לא נכונות. היו תקופות שפיתחתי ״J2EE״ וכתבנו Enity ו DTO ל 100% מהמחלקות, גם APIs שוליים שרק ביקשו מידע קטן, והיה להן צרכן יחיד – זה מיותר ומחליש את הבנת/חשיבות הצורך.

ברור לי שלאובייקטי הליבה במערכת שלכם (אלו שמשתמשים בהם המון, אלו שמעורבים בלוגיקה המורכבת והמשמעותית של המערכת) – חשוב מאוד ליצור Entity ו DTO.

מה עם אובייקטים חצי-חשובים? בשימוש לא-קטן אבל גם לא כבד? זה כבר עניין של ניהול סיכונים ותרבות ארגונית. למרות שמבחינת חישוב ROI פשוט (נשקיע השקעה קטנה בכתיבת Entity+DTO ל 20 מחלקות, אבל אחת שתידרש לזה באמת – תחזיר את ההשקעה בכתיבת 20 צמדים כאלו, כי זה חסך לנו Refactoring אחד גדול וקשה) ההשקעה משתלמת, קשה לפעמים לאנשים לראות את הערך ביחסי השקעה שכאלו.

לפעמים שווה להשקיע במקומות המסוכנים בלבד, ולספוג מעט ״נזק״ – אבל לשמר את העובדים עם תחושת ערך ברורה. שהם מבינים בבירור מדוע במחלקה מסוימת ההשקעה משתלמת – ושם משקיעים. פעם בכמה חודשים שיהיה Refactoring יקר (אבל לא מדי – כי זה לא אובייקט ליבה) – יזכיר לאנשים את הערך ב DTO+Entity.

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

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

כאשר המנטליות היא ״אנחנו חיים את היום, אחרינו המבול״ – אין סיבה שאנשים יראו את הצורך בכתיבת DTO+Entity, אבל אז הבעיה שלכם היא אחרת, וגדולה יותר.

הערה אחרונה: אפשר לכתוב גם לפעמים רק Entity או אולי רק DTO. אם אתם יודעים לאבחן מתי – אדרבא.

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

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

———

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

מצגת: העקרונות האוניברסליים של הנדסת תוכנה

נתבקשתי ע״י אפרת וולפנר-תורג’מן (ידידה ותיקה) ודור מצפי, שמקימים בימים אלה את גילדת התוכנה בחברת סמסונג – לבוא ולדבר קצת על תכנון תוכנה (Software Design).

האתגר שלהם: מגוון רחב מאוד של אנשי תוכנה: FE, BE – קל להבין, אבל בגילדה שלהם יש גם אנשי Data Science / ML, Data Engineers, Embedded, ו Firmware. כולם כותבים קוד, אבל באמת צורות העבודה של המקצועות הללו היא שונה דייה, כך שלא קל למצוא ברמת הקוד דוגמאות הרלוונטיות לכולם.

האם דזיין הוא שונה? האם אפשר באמת להגדיר כללים זהים שיתאימו גם למפתחי FrontEnd, גם לאנשי Machine Learning, וגם לאנשי Firmware.

לקחתי על עצמי את האתגר – והאמת שהוא לא היה קשה.

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

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

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

כמה נקודות במצגת ששוות להתייחסות נוספת

למה אפשר לצפות מדזיין טוב?

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

איך נראה דזיין טוב?

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

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

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

דזיין הוא תהליך – ולא מסמך

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

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

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

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

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

מקווה שתמצאו פוסט זה שימוש!

Design By Example III: Abstractions – חלק ב’

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

בכל אחת מהחלופות, ננסה לבחון את ההיבטים הבאים:

  • פתרון הבעיה העסקית – רמה #0 ע”פ מודל בגרות התכנון (אני מניח שאת רמה #1 ~בערך~ כיסינו בפוסט הקודם).
  • הכוונה / הצהרת כוונות – רמה #2 ע”פ מודל בגרות התכנון.
  • גמישות עתידית – רמה #3 ע”פ מודל בגרות התכנון.
  • עקרונות תוכנה – האם אנו מפירים איזו עקרון מקובל? זה סימן למשהו שחשוב לבדוק.

כמה הערות לגבי גמישות עתידית של המודל (Predicted Variations):

  • Predicted Variations הוא עקרון שעשוי לתרום, אבל להזיק – יש כאן בבירור Tradeoff:
    • אפשור היום לגמישות עתידית – הוא הימור. אם לעולם לא נגיע לידי שימוש בגמישות הזו – הרי שבזבזנו זמן, וסיבכנו את המערכת. השקעה / סיבוך היום, שלא יגיע לידי שימוש בעתיד – הוא בזבוז ברור. יש שיטענו ש Predicted Variations הוא דרך מבטיחה ל Overengineering.
    • גם השקעה היום, שניתן לבצע באותו עלות בעתיד (נאמר: שבוע עבודה היום, מול שבוע עבודה עוד שנה) – היא בזבוז.
    • השקעה משתלמת היום תהיה כזו ש:
      • חוסכת משמעותית עלות בעתיד. למשל: שבוע היום, מול חודש עוד שנה.
      • לחלופין: עוזרת להכווין את הדרך / לשמר אופציה עתידית חשובה. אולי תמיכה באנדרואיד (subsystem) ב Windows 11 היה קל לפתח בהתחלה ובסוף באותה המידה, אבל הצבת היסודות בשלב מוקדם מחדדת לכולם את המסר שזה כיוון אסטרטגי – ועוזרת לבדוק שפיתוחים אחרים אינם “חוסמים” את היכולת הזו.
    • כבני-אדם, בוודאי שאנו נוטים להערכת יתר של אפשרויות עתידיות. בדקו את ההיסטוריה של ההחלטות שלכם: אם אחוז ניכר של “ההכנות למזגן” (כינוי לא-מוצלח לגמישויות עתידיות) שיצרתם – לא הצדיקו את עצמן בבירור, זה סימן חזק שכדאי לכם להיות שמרנים יותר בהערכות העתיד שלכם. כל פיתוח שניתן לדחות לעתיד – עדיף. פיתוח שניתן לדחות – ולא יהיה בו צורך, על אחת כמה וכמה.

חלופה 1

  • פתרון הבעיה
    • חסר הטיפול במקרה-הקצה של שאלה המופיעה ב Entity Hub.
      • אולי זה מקרה מספיק פשוט לפתור בהמשך הדרך, שלא סביר בכלל שישנה לנו את התכנון בצורה מהותית – ואולי זה בדיוק הדבר שעלול לסבך אותנו בעתיד. אני הייתי מעדיף לסגור את הנושא הזה לא בסבב הראשון של התכנון – אבל בהחלט לפני הגעה למימוש.
  • הכוונה
    • יש חולשה בייצוג של EntityHub המכיל רשימה של דפים. אנחנו לא אומרים כלום על הקשר בין הדפים הללו (מלבד שיש להם סדר) או על הדמיון הבלתי-נמנע בין השאלון כולו (Questionnaire) לסט הדפים הללו (שקל לדמיין אותם כ “sub-questionnaire” מאיזשהו סוג. בעצם אי אמירה על הקשר – אנחנו אולי אפילו רומזים שאין קשר בין השניים, ומובילים את הבאים אחרינו ליצור שני מנגנונים שונים.
      • ההחלטה ששאלון ו”שאלון ל Entity” צריכים להיות שונים – היא הכוונה. למשל המבנה הבא מספק אמירה: (אם היא רצויה – אדרבא)
    • המונח Step (“שלב”) היא הפשטה גבוהה. כלומר: מתירה הרבה מקום לדמיון: האם popup בנוסח “לא ענית על כל השאלות, האם תרצה להמשיך בכל זאת? כן/לא” הוא שלב? האם ייתכנו שלבים ללא ייצוג ויזואלי? (למשל: שמירת נתונים, בדיקת אימות בצד השרת)? האם לחזור לדף קודם הוא שלב? אולי זה מתאים, ואולי לא – חשוב לשים את הדעת על הבחירה הזו, בהפשטה גבוהה.
      • נדבר שוב על ההפשטה הזו בחלופה 2.
    • גם Element היא הפשטה גבוהה. בעצם – ברמה הגבוהה ביותר. “אלמנט” הוא אפילו יותר מופשט מ”אובייקט” (שבעולם מתייחס בדרך כלל לדבר פיסי, ולא לרעיון). נראה בחלופה 3 לאן זה לקח אותנו.
  • גמישות עתידית
    • הייצוג של תת-השאלון ל Entity כרשימה של דפים – מגבילה בראייה של גמישות עתידית. אולי יש צורך כזה, ואולי לא.
      • שווה לראות את הגישה של חלופה 4 לעניין.
  • עקרונות תוכנה – אני לא מזהה חריגה.

הפשטות גבוהות מול הפשטות נמוכות

בשנות ה 80 ו ה 90 העליזות, של C, Cobol ו Pascal – מתכנתים כמעט ולא השתמשו בהפשטות, ופספסו הזדמנויות מידול בקוד שלהם. תנועות ה Object-Oriented וה Patterns שינו את המצב מקצה לקצה – והיום יש רבים שעבורם “גנרי”, “הפשטה”, ו”גמישות” – הם בהכרח דבר טוב. חלון נפתח (מטאפורה לגמישות) בבית שלנו – היא גמישות חשובה, אבל חלון נפתח שבתוכו חלון נפתח ובו עוד חלון נפתח – הוא כנראה מתכון לגמישות מיותרת שבעיקר תעשה בעיות. חשוב למצוא את מידת הגמישות הנכונה לבעיה.

כאשר אנחנו מגדירים הפשטה בתוכנה אנחנו מאפשרים לבאים אחרינו לחלום ולדמיין וריאציות אחרות של המבנים שהגדרנו – שזה עשוי להיות מצוין. הפשטה גבוהה מדי – תגרום כנראה לנזק. הפשטה נמוכה מדי – לקיבעון וחוסר דמיון. אני נתקל בנטייה ברורה של אנשים לבחור הפשטות גבוהות מדי לצורך הנתון, מכוונה טובה, ואולי במחשבה שזה “more visionary” / עדיף. כמו כל דבר כמעט בתכנון תוכנה – הכי טוב להבין את רצף האפשרויות, ולמצוא את המידה הנכונה. הביטו בדוגמה הבאה, בה ארבע הפשטות שונות לסט הצורות: ריבוע, מלבן (rectangle), וטרפז. איזו הפשטה עדיפה?

זה כמובן תלוי. Visual היא הפשטה מאוד גבוהה. היא תגרום למפתחים באזור לדמיין ולגשת לאפשרויות אחרות שההפשטות האחרות, הנמוכות יחסית, לא יאפשרו. מצד שני – היא יכולה “להכשיר” עיוותים בלתי רצויים בעליל. למשל: Visual נועד לציין תוכן (content) על המסך, אבל ההפשטה הגבוהה מתאימה גם ל control (רכיבי שליטה) כגון כפתורים בתוכנה, מסגרת, tooltip – וכו’, וכך הדברים מתערבבים. שימו לב כמה הכוונה יש בכל רמה של הפשטה, ואיזה כלי משמעותי זה להכווין את הבאים אחרינו – להיכן אנו רוצים שהדברים יתפתחו.

אני מכיר את הטיעון הבא: “הפשטה גבוהה יותר אינה מזיקה, לאנשים יש שכל – והם לא יכניסו שם דברים לא מתאימים / לא יגיעו למסקנות שגויות”. הניסיון שלי לאורך שנים רבות מראה בדיוק את ההיפך: יותר מדי פעמים ראיתי איך הפשטות גבוהות “הכשירו” ו/או הקלו על המצפון ביצירת עיוותים במערכת, שלאורך היו יקרים מאוד לתיקון.

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

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

חלופה 2

לחלופה הזו יש הרבה חפיפה עם חלופה 1. נתמקד בשני ההבדלים המהותיים:

  • פתרון הבעיה העסקית – פותרת.
  • הכוונה
    • כל Step מכיל Elements. זו בעצם הגבלה – ההיפך מהפשטה.
      • ניתן להתפשר ולהחזיק רשימות ריקות / null כאשר לא נדרש – אבל המשמעות היא קוד מסורבל יותר, ומסר הרבה פחות ברור לגבי הכוונה.
      • עצם כך שרוב ה Entity Hubs (ע”פ ה narrative מהפוסט הקודם) לא יכללו אלמנטים – ואנחנו פה קובעים שכל Step מכיל Elements – בעצם יצרנו כלל שרוב הפעמים אינו נכון. זו הכוונה הפוכה. אפשר לומר: כמעט הטעייה.
        • כשדורשים מאתנו לחבוש מסיכות (רפואיות) תוך כדי אכילה – כנראה שנסיק שמי שקבע את הכלל לא ממש מבין. כאשר אנחנו נתקלים בהכוונה הפוכה – שמתנגשת עם המציאות – קורה אותו הדבר. עולים סימני שאלה לגבי איכות ההכוונה.
      • יש סתירה ברורה בין ההפשטה הגבוהה (“Step”) לבין ההגבלה שכל Step כולל Elements. נראה שניסו לקרוא ל Step בשם טיפה יותר מצומצם “QuestionnaireStep”, אבל מכיוון ש Step מוחזק ע”י Questionnaire – לא נראה שנוספה כאן משמעות (מקסימום השם עומד טוב יותר בפני עצמו). ככל שההפשטה גבוהה יותר, נצפה לפחות קביעות (הגבלות) על הפשטה. הגבלות / קביעות על ההפשטה הוא כלי שימושי להכוונה – אבל במקרה הזו זו פשוט נראית הכוונה לא טובה.
  • גמישויות עתידיות
    • EntityHub מכיל QuestionnaireStep – ולא Pages.
      • זו בעצם גמישות, שמאפשרת עץ מקונן של דפים ו EntityHubs.
      • הקשר בין EntityHub ל Page הוא פחות ברור אפילו מחלופה 1 (קשר עקיף).
      • אם יש צורך עסקי אמיתי באופק למבנה כזה – ייתכן וזה מודל טוב. על פניו מהתיאור בפוסט הראשון – זו נראית כמו גמישויות מיותרת המטשטשת את הכוונה.
  • עקרונות תוכנה – אני לא מזהה חריגה.

חלופה 3

החלופה הזו צורמת בעיני מהמבט הראשון, מכיוון שהיא מפירה את עקרון ה SLAP (Single level of abstration principle), מה שגורר הפרה של עקרון ה (POLA (principle of least astonishment. אני יודע בקרב המגיבים לפוסט הקודם – זו הייתה האופציה המועדפת, ואני מוכן להגן על עמדתי. טיעון שהועלה הוא “פשטות”, ואכן פשטות הוא יתרון אמיתי – אבל אני אנסה להראות שהפשטות שהחלופה הזו מציגה היא בעיקר מראית-עין, ולאורך זמן אני מעריך שהיא לא תחזיק מעמד. מצד שני – בצד ההכוונה, דווקא יש סיכון ממשי להכוונה לכיוונים לא מועילים. אפרט.

  • פתרון הבעיה העסקית – פותרת.
  • הכוונה
    • כפי שציינתי כבר בחלופה 1, המונח “Element” מספק הפשטה מירבית[א], מה ש”מתיר” להכיל: כלב, עץ, עוני, ורקורסיה – כ Elements נוספים במערכת. המונח Element לא סותר / דוחה את האפשרויות הללו מעצם שמו.
      בקיצור: הפשטה מירבית היא הכוונה אפסית. אין פה הכוונה. הכל הולך.
      • מה היה אפשר לעשות אחרת? לספק הכוונה מסוימת. למשל, השם “QuestionnairePageElement” כבר מגביל / מכווין אותנו הרבה יותר טוב. גם כלב, וגם רקורסיה – כבר בבירור אינם מתאימים. EntityHub – פחות מתאים, אבל עדיין יכול “להשתחל” עם קצת דמיון (כ “iframe ויזואלי”). אם היינו קוראים ל EntityHub בשם EntityPage – זו הכוונה נוספת, כי זה לא נשמע טבעי להכיל page בתוך page. מונח כמו “QuestionnaireComponent” יכול להיות הכוונה, אם המונח Component מתקשר אצלנו חזק לרכיב UI עצמאי (כך ב UI frameworks מסוימים). בקיצור: הייתי מנסה להחליף את המונח Element במונח שמכווין יותר את הכוונה.
  • גמישות עתידית – יש אפשרות להוסיף כמעט כל דבר כאלמנט – מה שנוגע בנקודה הבאה.
  • עקרונות תוכנה
    • כותרת (Title), שאלה (Question), תמונה (Picture), ועמוד ניהול ישויות (Entity Hub) הם לא באותה רמת הפשטה. אני מניח שזה בולט ברמה של תרגילי “מצא את יוצאי הדופן” הפופולריים בחוברות עבודה של הילדים שלי כשהיו בגילאים מוקדמים. (לא פעם אגב, הרגשתי לא שלם עם התשובה שהחוברת מציעה ל”יוצא הדופן”).
      • הם בסדרי גודל אחרים: חייל בודד מול פלוגה.
      • הם עצמאיים במידה שונה: אחד זקוק ל Container / מסגרת שתכיל אותו – והשני לא.
    • נטען שהכנסת כל הנ”ל לאותה הפשטה תאפשר קוד פשוט יותר (ריבוי-צורות / polymorphism) – אבל ריבוי-צורות לא עובד בפועל, כאשר הרכיבים השונים בו לא דומים מספיק זה לזה. התוצאה לרוב היא branching הולך וחוזר בקוד:
      • if type = EntityHub -> do x
      • else -> do y
    • כלומר: יצרנו הכללה (“Entity”) לפריטים שזקוקים לטיפול שונה מהותית, ולכן למרות היכולת להכיל אותם באותו מבנה נתונים (<List<Entity, למשל) זה לא יעבוד ברגע שנטפל בקוד אחרת – ובעצם נטפל, ברוב המקרים, בשתי קבוצות שונות של פריטים. כלומר: כאילו הייתה לנו הכללה, אבל בפועל הקוד נאלץ לטפל בשני מקרים נפרדים.
    • הבעיה הכי גדולה, היא “ההזמנה” להוסיף כל פריט נוסף להכללה הגבוהה של “Entity”. מכאן הקוד ילך ויסתבך. גם ב branching גדול יותר בקוד, אפילו יותר – באי-חלוקת הקוד לנושאים / אזורים מופרדים (אותו מחלקה תטפל בכל הסוגים השונים של הפריטים), והכי גרוע – פספוס ההזדמנות לחלוקה יותר הגיונית והכוונה יותר טובה של האזור הזה בקוד – לו היינו משתמשים בהפשטות טובות יותר.

חלופה 4

טוב, אני חייב להודות שזה המודל המאוזן והפשוט ביותר לטעמי, ע”פ הבנתי של ה narrative. עדיין יש לו חסרונות, בואו נראה:

  • פתרון הבעיה העסקית – לא טיפלנו בשאלה על EntityHub – וזה חסר.
  • הכוונה
    • כפי שציינתי, לפי דעתי הכי פשוט ומאוזן מכל החלופות האחרות:
      • Step הוא אחד משני מצבים – הנבדלים זה מזה.
      • EntityHub בעצם קשור לשאלון, ישות שמשמעותה ברורה.
        • כן הייתי מצפה שיכולות הנוספות לשאלון, ייתמכו גם ב”תת-השאלון”. אני מניח שגם משתמשים לא היו מבינים למה שרמה 0 (שאלון-העל) יש התנהגות אחת, ובתת-שאלון (רמה 1) – יש התנהגות אחרת.
    • עדיין Entity היא הפשטה גבוהה מדי, וגם Step עדיין פתוח לפרשנות (לטוב ולרע – תלוי למה אנחנו מתכוונים)
  • גמישות עתידית
    • בחינתי הצד הטוב הוא שימוש חוזר ביכולת ה Questionnaire גם לתת-שאלון.
    • הקוראים ציינו שהגמישות להכיל היררכיה של EntityHubs אינה נדרשת – והיא נראית כגמישות מיותר. אני מסכים – ומעדיף לחסום אותה.
  • עקרונות תוכנה – אני לא מזהה חריגה.

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

ככלל: כשאנחנו בוחרים בין חלופות, אל לנו להפסיק בבחירת החלופה. ניקח את החלופה הטובה ביותר בעינינו – ונפתח אותו כך שתהיה טובה אפילו יותר.

  • ניסיתי להגביר את ההכוונה בעזרת מונחים המובילים להפשטות נמוכות יותר:
    • Questionnaire Page במקום Step. לא נראה שצריך משהו יותר מזה בשלב הזה. להגביה את ההפשטה בעתיד – לרוב קל יותר מאשר להנמיך הפשטה.
    • Component במקום Element – בהנחה שברור שזה רכיב ויזואלי בודד בדף. זה שינוי חשוב בעיני.
  • הוספתי ל EntityHub Page שאלה אחת אפשרית. כלומר: יש טיפול מיוחד (אי שימוש חוזר בקוד ה Component) בשאלה על EntityHub – אבל זה נראה לי האופציה הפשוטה יותר בסה”כ.
  • הגדרתי שני סוגים של Questionnaire כדי לחדד שלא כל תכונה / יכולת של ה Root Questionnaire תהיה בהכרח ב Sub-Questionnaire, למנוע שלא נסתבך.
  • הוספתי constraint על ההורשה ש Sub Questionnaire לא יכיל Entity Hub Pages. אין צורך כזה – וחבל לסבך את המערכת.
    • איך ממשים את זה? תלוי בשפת התכנות. ניתן לבודד את Sub-Questionnaire שיחזיק רק QuestionnirePages – אבל אני חושש שהתרשים קשה יותר לקריאה:
  • אני שומע כבר ביקורת עולה: אבל הפתרון שלך יותר מסובך מכל האחרים. זו פשטות???
    • אני טוען: התרשים מורכב יותר – לא הפתרון. בכל מקרה בקוד (שיהיה מסובך עוד יותר, אני מניח) – נתמודד עם השאלות הללו. אני מעדיף לפתור אותן בשלב התכנון, ואני מניח שהתרשים המפורט / מורכב מעט יותר – בסה”כ יתרום להבנה משותפת של מי שעובד על הפיצ’ר. השאלות הגדולות הן שם – ובאופן דיי פשוט, לדעתי. למנהלים בכירים אפשר להציג בתור התחלה תרשים מופשט יותר (כמו התרשים של חלופה 4, עם מונחים המובילים להפשטות פחות גבוהות)

סיכום

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

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

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

מטרת הפוסט לא הייתה לדון בפתרון כזה או אחר – אלא בדרך להגיע לפתרון.

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


[א] – אני מודע לכך ש”מירבית” הוא כתיב לא תקני – אבל הוא נראה לי ברור יותר. כמו שפרי ברבים צריך להכתב פרות (Peyrot), אבל הגיוני יותר עדיין בעיני לכתוב פירות.

Design By Example

הרבה זמן אני מתחבט בשאלה: כיצד לומדים (או מלמדים) Software Design בצורה יעילה?

לא קשה ללמד סט של 20+ Design Patterns – אבל בטווח הקצר (ואולי גם הבינוני) זה יוצר בממוצע יותר נזק מתועלת – כי הדגש הופך להיות “למצוא הזדמנויות לממש Design Patterns”, ולא “לפתור בעיה עסקית”.

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

ישנם סגנונות ארכיטקטוניים שאוהבים ללמד (Layered, Microservices, Event-Driven, וכו’) – שזו בטוח נקודת מבט חשובה, ויש Quality Attributes – טוב ונחמד, אך עדיין לא מדריך כיצד לעשות Design נכון.

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

“המרכיב הסודי” הוא כנראה שילוב של:

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

אני חושש שללא הארבעה הללו, או לפחות שלושה מהם – קשה להגיע לתכנונים מוצלחים בעקביות ולא משנה כמה ידע תאורטי / UML / SysML / Patterns / Architecture Stytles – למדתם לעומק.

ידע מקצועי, ותאוריה של Design הם חשובים – אבל לא מספיקים. הייתי אפילו מנח ש Design מוצלח מורכב בערך שווה בשווה בין “המרכיב סודי” (4 הנקודות הנ”ל), ידע כללי בתוכנה, ותאוריה של עיצוב תוכנה.

התאוריה של עיצוב תוכנה (Patterns / Quality Attributes / Styles / Principles) בעיקר עוזרת לנו להעריך אפשרויות במהירות, ולהבין טוב יותר מה הנקודות החזקות והחלשות בכל אופציה – כדי ליצור / להמציא אפשרויות נוספות, וטובות יותר.

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

The Classical URL Shortener Question

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

  • כולנו (אני מניח) היינו לקוחות בשלב כזה או אחר של URL Shortner, אז יש לנו מושג ברור במה מדובר, מה הצורך, מה חווית השימוש, וכו’.
  • זו בעיה שקל לסגור בה “לופ ראשון” מהר. כלומר: תהליך של דזיין הוא להתחיל בפתרון נאיבי ולשכלל אותו, וכאן הפתרון הנאיבי הוא קל ביותר. ברור שלכתוב URL Shortner ב scale גבוה כך שיהיה יעיל ואמין – זו בעיה קשה.
    • הטעות הנפוצה ביותר אולי בקרב “מתכננים מתחילים” (בגילאי 20 עד 99) היא לצלול לפרטים ולאזור מסוים לפני שיש תמונה שלמה / מלאה מספיק טובה.

הנה השאלה:

“נניח שאנחנו רוצים לבנות URL Shortener בחברה שלנו, שלוקח URL ארוך, למשל https://softwarearchiblog.com/wp-admin/post.php?post=3658&action=edit ומקצר אותו ל URL קצקצר כמו https://short.com/xyz1234. איך היית מתכנן שירות כזה? בוא תתחיל/י לתאר בבקשה”

איך מתחילים להתמודד עם שאלת דזיין?

מה דעתם?

אולי הכי נכון לפתוח בבחירת סגנון ארכיטקטוני (microservices או event-driven, אולי space-based)?
אולי להיזכר בכל עקרונות ה SOLID ולראות איך לממש אותם?
אולי בעצם – פשוט להיות פרגמטי, ולחשוב על המימוש – ולבחור טכנולוגיה מתאימה, למשל Spring Boot או Vert.x?

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

אז מאיפה מתחילים? משתי נקודות המוצא הבאות, ובמקביל:

  • בירור הצרכים הפונקציונליים והאיכותיים מהתוכנה. יש הבדל דרמטי בין URL Shortener שיתחרה בזה של Bit.ly בנפח הבקשות שמטפלים בהן, לזה הפנימי של החברה – שאולי לא צריך לטפל יותר מכמה אלפי בקשות ביום. כנ”ל לגבי אבטחה, עלויות, retention וכו’.
    • אני מדגיש את המונח צרכים על פני דרישות – כי דרישות הן פרשנות של הצרכים, שחשוב לבקר ולאתגר את הדרישות, ולא לקבל אותן כאמת מוחלטת. חלק מטעויות ה Design הכואבות ביותר שראיתי היה קבלת הדרישות בלי שאלות – היכן שכמה שאלות הגיוניות ופשוטות, היו יכולות להוביל לנתיב אחר לגמרי.
  • לסגור לופ ראשון, נאיבי, כמודל ייחוס שאפשר להתחיל ולעבוד ממנו באיטרציות.
    • זה לא רק בסדר, אלא נכון יותר להתחיל במודל פשוט.
      • אם נתחיל במודל בסיסי/פשוט – יהיה לנו קל יותר להתמקד בעיקר ולא להיגרר לאופטימיזציות של דאגות משניות (טעות נפוצה), או להיצמד לטכנולוגיה או מבנה מוכר – גם כאשר הוא אינו רלוונטי.
      • אנו חותרים לפתרון פשוט – ועדיף לנסות ולהימנע מכל שיכלול שאינו נדרש. מערכות פשוטות יותר – כושלות פחות, וקל יותר לבצע בהן שינויים עמוקים.

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

אני אפתח בסגירת לופ ראשון, כי אני רוצה טיפה יותר להבין את הנושא לפני שאשאל שאלות על דרישות. הנה מה שיצא לי, פתרון נאיבי אבל שסוגר “לופ”:

התכנון הבסיסי הזה עוזר לי להבין כמה דברים בצורה יותר ברורה:

  • למשל, שמדובר ב 2 endpoints: יצירת shortURL, ופיענוח שלו.
  • שצפוי כנראה שהשירות גם יעשה redirect ל shortURL – כלומר, הוא יקבל traffic ישיר מהאינטרנט ולא יקבל רק בקשות לפענוח. כמובן שאני יכול להפריד את האחריות הזו לרכיב אחר, אבל אני יודע שזו דאגה מתקדמת יותר, שאין טעם לכלול אותה בדיון בשלב הזה… רק התחלנו. לעתים אנחנו מנסים להפגין ב Design שלנו חשיבה על כמה שיותר אפשרויות. נחמד לכתוב אותן בצד – מזיק להטמיע אותם ישר ב Design, עוד לפני שהובן שיש צורך – כי כך אנחנו מסבכים את ה Design וכנראה שלא לצורך.
  • כתבתי ג’אווה ו RDBMS – כי זו הסביבה שהכי טבעי לי לחשוב עליה.
    • תוך כדי שאני מביט בתרשים אני חושב שאם מדובר ב hyperscale אולי עדיף כשפה את Rust (ללא GC) ואולי בסיס נתונים Key-Value שיכול to scale out למספר nodes בצורה אמינה.
    • שוב: נחמד לחשוב את זה, אבל טעות גדולה (ולצערי: נפוצה) היא להתחיל להטמיע את המחשבות הללו ב Design לפני שברורים לי הצרכים. אין Design טוב בצורה אבסולוטית. Design הוא מוצלח רק יחסית לצרכים. לצרכים שונים תכנונים שונים יהיו טובים או גרועים.

מתוך התכנון הבסיסי, אני מרגיש נוח יותר לדבר על צרכים / דרישות:

  • שאלה הכי משמעותית ל Design כנראה היא שאלת ה sclae: בכמה URLs אני צפוי לטפל? בכמה בקשות כל אחד מה endpoint יתמודד בשעה (למשל), כמה shortURLs אצטרך לשמור? מיליונים? מיליארדים? יותר?
  • האם יש הנחות מסוימות לגבי מבנה ה URL? הם מגיעים מ domain מסוים / מאפיין מסוים? אם המערכת היא פנימית לחברה יותר סביר שיהיו הנחות כאלו – שיכולות לתת לי leverage אמיתי ב design. אם אני יוצר מתחרה ישיר ל bit.ly/tinyUrl – אז זה פחות סביר.
  • לכמה זמן אני צריך לשמור את ה shortURL? לזמן נתון (נניח 30 יום – מספק צורך נקודתי), או לעד (שירות כללי)? אני מניח שלעד משפיע ממש על מודל עסקי, כי הנזק מיצירת מיליארדי shortURLs ואז אי תמיכה בהם יום אחד בהיר – יפגע בהרבה מאוד אנשים. כאן הייתי רוצה מבנה עלויות מינימלי שיאפשר להמשיך את האופרציה של תמיכה ב shortURLs שכבר נוצרו, לזמן ארוך.

מראיינים שראיתי (אני מכיר את השאלה הזו כבר כעשור) נהגו להפוך אותו לשאלה של Hyperscale: “עליך לתמוך בעד 1,000 מיליארד URLs, עם 100 מיליון בקשות ביום.” זה לא מציאותי (או לפחות תאורטי-מדי), כי גם שמגיעים כאלו scales -מתכננים חכמים לא מתחילים בתכנון ל scale מרבי. עוברים שלב-שלב, מדרגה-מדרגה. ארגונים רציניים ישקיעו בתכנון יותר משעה, ולא יטילו את המשימה (לרוב) על העובד שרק הצטרף לחברה. ניחא.

בואו נבחר דרישות שדורשות לחשוב על Scale, אבל מבלי הצורך להתמודד עם נקודות קיצון (של scalability):

  • הטיפול הוא ב URLs מכל סוג, ע”י משתמשים אנונימיים.
  • נתכנן מערכת שתהיה מסוגלת לשמור עד 100 מיליון URLs, קצב קידוד של מיליון URLs בחודש, וקצב פענוח גדול פי 20: 20 מיליון URLs בחודש.

איטרציה שניה

מה בתכנון הבסיסי שלנו אינו מספיק-טוב לדרישות?

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

שרתים, מן הסתם נרצה יותר מאחד: 2-3 instances לפחות עבור High Availability, ואפשר לגדול עוד, אם תהיה עוד עבודה. קידוד של מיליון URLs בחודש, זה ממוצע של כ 35-30 אלף ביום או 1500 בשעה, פחות מאחד בשנייה – לא נשמע מאתגר, גם אם נניח שבשעות העומס יש פי 5 traffic מהממוצע.

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

מה הייתי עושה אם לא היה לי את הידע הזה? הייתי מתחיל לעשות load testing למערכת – ומגלה. באיחור של כמה שבועות את סדרי הגודל. עיכוב כזה הוא חיסרון גדול – אבל לא מונע ממני מגיע לשם. הרבה פעמים ידע הוא זרז (משמעותי) – אך חסרונו אינו showstopper.

כמובן שאני רוצה מספר שרתים (server instances) – גם אם שרת אחד מספיק חזק לטפל בכל הבקשות, עבור High Availability. אני מניח שהיום זה כבר common sense.

איך התכנון שלי עומד במדדים של Design? קרי SOLID/GRASP או עקרונות מסוימים? המבנה כ”כ פשוט שלא נראה לי שיש עקרונות שהוא ממש יכול לסתור. פוריסטים עשויים לטעות ששירות אחד בג’אווה שמבצע שתי פעולות: קידוד ופענוח של URL זה לא SRP – אבל אנחנו לא פוריסטים. כמטאפורה: עפרון עם מחק בקצה זה שימושי וטוב – ואני לא מרגיש צורך להפריד בין השניים “כי אלו שני כלים שונים, ואנחנו עושים בלאגן – כאוס ממש”.

עד כאן לא הרבה השתנה ב Design:

עכשיו אני צריך להעמיק שלב אחד הלאה בפרטים: כיצד יעבדו ה endpoints? מה הם יעשו?
תהליך יעיל של Design הוא כזה שאני עושה איטרציה מהירה לסגור “לוף” (כלומר: end-to-end) מסיק מסקנות / בוחן חלופות, ורק אז נכנס לעוד רמת עומק / פרטים. זהו תהליך איטרנטיבי ביסודו.

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

זה רעיון שמאוד קל לי לחשוב עליו ולתאר אותו – אבל נראה שזה לא ה common sense, ולכן אני חוזר על הנקודה הזו כמה פעמים: צלילה מהירה לפרטים מוקדם מדי, התקבעות על רעיונות לא הכי פשוטים שנשמעים “יותר חכמים” (ניקח NoSQL Database, שפת סקאלה, בחירה ב multithreading model כזה או אחר) – זו הדרך הלא נכונה לעשות את הדברים. סיכוי טוב שאין לאופטימיזציות הללו יתרון ממשי, אבל הם מקבעים אותנו על פרטים מסוימים, שיוצרים מגבלות / סוגרים אפשרויות (למשל: scala דורש JVM, בסיס נתונים K/V מגביל אותנו ביכולות חיפוש או דורש מאתנו עוד רכיבים כדי לאפשר חיפוש יעיל) ומרחיקים אותנו מבחינת האופציות העקרוניות – שהיא החשובה ביותר בשלבי ה Design.

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

איך יעבוד ה endpoint של קידוד long URL? יש פה כמה אלטרנטיבות שעולות מיד:

  • ה Short URL הוא Hash על ה Long URL.
  • ה Short URL הוא GUID (מזהה אקראי / בלתי תלוי).

אני מדבר כמובן רק על ה “id” של ה shortURL, קרי: <https://short.com/<id
לא ברור לי מיד איזו אלטרנטיבה עדיפה, ואני שמח שיש לי יותר מאחת. אני אקדיש את הזמן להשוות ביניהן.

  • hash היא פונקציה “סטטיסטית” וייתכנו שני long URLs שונים שיניבו אותו hash.
    • ההסתברות תלויה באיכות ה hash function, גודל ה hash שנוצר, וכמות האיברים שאקדד – אבל בכל מקרה “התנגשות” היא בלתי נמנעת.
    • איך אפשר לטפל? זה ידרוש ממני בכל קידוד לגשת לבסיס הנתונים, לראות אם קיים ה hash הזה והאם הוא מצביע לאותו long URL, ואם לא – לספק איכשהו id אחר, עם לוגיקה שניתן לשחזר כאשר ה LongURL הזה מופיע שוב. אפשרי – אבל זה אומר קריאה מבסיס הנתונים בכל קידוד, וקצת סיבוכיות בטיפול בהתנגשויות.
  • GUID הוא גם סטטיסטי, אבל מספיק גדול שלא סביר שייווצרו איי פעם שניים כפולים. מצד שני, GUID תקני הוא באורך 32-36 תווים, מה שאומר שה URL שלי כבר לא כ”כ קצר. מזיכרוני כמשתמש נראה ש bit.ly לא מייצרים id ארוך ביותר מ 7-8 תווים.
    • mitigation אפשרי הוא להשתמש ב GUID קצר יותר, אך חלש יותר – עם הסתברות הולכת וגוברת ל”התנגשות”.
    • חיסרון נוסף הוא ש Long URLs זהים שיתקבלו לקידוד, יקבלו כל אחד GUID חדש – וכך לא יהיה שימוש חוזר ב shortURL, אלא אם נפנה לבסיס הנתונים ונחפש אם ה URL הזה כבר קיים בכל פעולת קידוד.

שוב, אגב, אני מתבסס על ידע (הבנה כיצד פונקציות hash עובדות, או GUID).
נראה ששתי האופציות שעומדות בפני הן יותר נקודות על רצף מאשר גישות שונות שמובילות ל tradeoffs שונים בעליל. איך מחליטים?
נחזור לדרישות ונבחן את האופציות דרכן: shortURL עם id של 32 תווים לא נשמע לי רצוי אם אנשים אמורים להקליד את ה URLs הללו. בתסריטים מסוימים זה עשוי להיות סביר.
מצד שני: טיפול בהתנגשויות גם נראה לי לא דבר רצוי – סיבוכיות.

אני רוצה URL קצר ככל האפשר, ומעדיף את גישת ה GUID שחוסכת לי טיפול בהתנגשויות. בכל מקרה בשתי האפשרויות ארצה לגשת לבסיס הנתונים בכל קידוד לבדוק אם ה LongURL כבר קיים: גם עבור שימוש חוזר ב shortURL וגם להתגבר על התנגשויות. ברור שלבחירה הזו יש מחיר בביצועים – אבל אני עדיין לא מתרשם שהגיע הזמן לעשות הקרבות בפשטות / פונקציונליות עבור הביצועים.

אני חושב שהדרך ליצור URLs הכי קצרים הוא בבסיס הנתונים לנהל auto-increment ואת המספר לקדד לתווים אלפא-נומריים. נניח יש ערכי ה ASCII שזה 256 תווים אפשריים, אני משתמש ב modulo על מנת לחלץ את המספר על בסיס 256. ה ids הראשונים שיתקבלו יהיו תו אחד (a, b, c) ואם הזמן ילכו ויתארכו ככל שישתמשו במערכת יותר. מאיפה זה בא לי? אינטואיציה / ניסיון, אני מניח.

הנה המצב שהגענו אליו:

endpoint 1 בעצם גורר שתי פעולות: 1.1 ו 1.2.


שינוי קטן לסכמה: הפסקנו לשמור shortUrl כי בעצם id של ה shortURL הוא ה autoinc בבסיס 256. כשאני מקבל id בבסיס 256 אני יכול להמיר אותו למספר בבסיס 10 (autoinc) בפעולה חשבונית פשוטה. חבל לשמור את זה בבסיס הנתונים. שווה לציין ש primary key קטן יותר (בבתים) – גם ישפר את ביצועי בסיס הנתונים.

כמובן שכל זה מתאפשר בעקבות שימוש בבסיס נתונים יחיד ומרכזי. אם היינו נאלצים להשתמש בבסיס נתונים מבוזר (עבור scale) – autoinc מרכזי כבר לא היה עובד והיינו נאלצים להשתמש בגישה אחרת: GUID/Hash שהייתה מניבה URLs ארוכים יותר, או אולי פשוט מקצים לכל שרת “bulk” של מספרים ייחודיים שהוא רץ איתם והוא יכול לקבל bulk נוסף – כאשר נגמרו לו המספרים המוקצים (ואז עדיין ה URL יהיה קצר כמעט ככל האפשר).

נעבור לבחון מעט יותר את ה endpoint השני.

ה endpoint השני: shortUrl => Redirect to longURL

כאן היישום נשמע דיי פשוט:

  • קבל shortUrl בקידוד ASCII והמר אותו לבסיס 10 (autoinc).
  • חפש בסיס הנתונים את ה URL המלא.
  • החזרת תשובת redirect (קרי HTTP 302) עם ה longUrl.

משהו נשמע כאן מוזר? אי אפשר להעביר את רוב תווי ה ASCII על גבי URL – זה לא תקני ודפדפנים לא יקבלו את זה (שוב: ידע). פספסנו את זה.
נחזור ונשנה גם את ה endpoint הקודם לא לקדד על בסיס 256 (ASCII) אלא על בסיס של תווים שמותרים ב URL, למשל a..zA..Z0..9 שזה 62 תווים, כנראה שיש עוד קצת מותרים ששווה להשתמש בהם וככה להגדיל את הבסיס (ולקצר עוד קצת את ה URL). שימו לב ש URL הוא case sensitive ויש הבדל בין אות גדולה לאות קטנה (ידע).

איטרציה שלישית

בגדול אני דיי מרוצה מהפתרון שהגענו אליו, הוא נראה ישים ופשוט – ובהחלט משהו שאפשר לצאת איתו לעבודה, ולהתחיל “להרגיש את השטח”. מה חסר בפתרון? כלום – אם לא צריך יותר.
בכל זאת, מה הצד החלש של ה Design הנוכחי שלנו? מה יכול “להפיל” אותו?
אני לא חושב על משהו פונקציונלי – אבל בהחלט יכול להיות ש scale עשויה להיות בעיה.

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

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

לא פעם ראיתי אנשים טועים בבעיות Design בסיסיות, ולא מוכנים לקבל פידבק על “היצירות שלהם”. יוצאים לפרודקשיין רק כדי ללמוד שם שזה לא עובד, ולהתחיל איטרציה מחדש. אני מעריך יותר מנהלים שיודעים אז לומר: “אני מאמין שהיה אפשר לעשות טוב יותר, אני רוצה שנלמד מזה” מאלו שנותנים למפתח שוב ושוב להיכשל בפרודקשיין, לעבוד על הפיצ’ר פי 4 מהנדרש – ובסוף משבחים אותו על העבודה הקשה והמסירות שהפגין. חכמת חלם.

חזרה ל Design: אמרנו שהנקודה הפוטנציאלית של ה Design נראית טיפול ב scale. איך נשפר את ה Design שלנו להיות מוכן יותר ל high scale?
גישה אחת, פחות רצינית, היא “לעבור להשתמש בכלים של scale”: למשל: Cassandra, Scylla, אולי ZooKeeper וכו’.

אם אתם לא מכירים את הכלים האלו לעומק, ואתם מתבססים על “סיפורי הצלחה” שאתם לא מבינים – עדיף לעצור כאן ולא להתקדם. קצרה היריעה מלספר על מקרים בהם אנשים השתמשו ב”כלים של scale”, אבל לא בכלים הנכונים – ובזבזו זמן רב (עד רב-מאוד) או ממש דרדרו את ה scalability של המערכת.

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

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

  • אם ה URL חוזרים על עצמם בצורה ניכרת (“blockbusters”) – אם בהפעלה (shortUrls מסוימים תופסים נתח מורגש מהשימוש) או בקידוד (המון משתמשים באים לקדד את ה URL שהוא google.com) – אזי caches בהחלט יכולים לעזור. Cache שיחסוך לנו גישה יקרה לבסיס הנתונים: אם בקריאת ה autoinc => longUrl או בחיפוש אחרי longUrl אם כבר קודד. שני caches שונים.
    • Central cache בנוסח Redis (או כלי אחר שאתם מבינים) יהיה יעיל ככל שמספר ה instances רב יותר.
      • כאשר יש Central cache יש מקום לשקול multi-layered cache כאשר יש minimal cache בזיכרון של כל שרת לגישה מהירה באמת, בלי פעולת רשת.
  • הפרדה בין פעולות שונות על מנת לבצע אופטימיזציה טובה יותר של משאבים לכל פעולה: אין צורך שאת הקידוד של longUrl ואת התרגום יעשו אותם שרתים – אפשר לפצל לשניים.
    • אפשר להוסיף לבסיס הנתונים read-replica רק לצורך התרגום (חיפוש לפי id) – וכך לאפשר לבסיס הנתונים לנהל את ה caches הפנימיים שלו בצורה יותר יעילה.
  • מעבר לבסיס נתונים יעיל יותר לצורך הפעולות הנתונות: בעצם אנחנו משתמשים בבסיס הנתונים רק לצורך key/value ויש בסיסי-נתונים שמתמחים בזה. כמה יותר יעילים הם יהיו? האם יהיה משתלם לעשות מעבר (ביצועים / עלויות תפעול / learning curve)?
  • מעבר לבסיס נתונים מבוזר – אם ורק אם בסיס נתונים יחיד מרכזי לא מצליח לעמוד ב capacity של ה URLs. נשתדל לא לשלם סתם על מה שלא צריך.
  • שיפורי performance: הבדיקה בכל endcoding אם קיים כבר longURL כזה היא התקורה הבולטת ביותר בעיני. אפשר לנסות ליישם טכניקות כגון Bloom Filter שמאפשר לייצג “חתימה מינימלית” של ה longURL בהרבה פחות מקום – מה שייכנס בקלות ל cache מקומי של השרת ואולי גם ל cache של המעבד.
    • פעם התחלתי לכתוב פוסט על מבני-נתונים הסתברותיים, אבל הסקתי שזה נושא נישתי שלא ייגע לרוב הקוראים…

סיכום

האם סיפקתי את ה Design הטוב ביותר לבעיית ה URL Shortner?

ברור שלא – כי URL Shortner הוא בעצם סט של בעיות דומות אך שונות. לכל צורך – משהו אחר ישתנה. למשל: אם הצורך הוא להחזיק URL רק 30 יום – כנראה שמבנה הביצועים ישתנה, ותיפתח לנו אפשרות “למחזר” URLs כדי ולשמור עליהם קצרים יותר לאורך זמן (?!).

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

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



לינקים רלוונטיים

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

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

מאמר של High Scalability על Bitly. לא לגמרי מה שציפיתי לו, האמת. חשוב לציין שהמאמר משנת 2014, קרי המערכת תוכננה כמה שנים קודם לכן, ואולי לכן היא נשמעת מעט מיושנת.