Site icon בלוג ארכיטקטורת תוכנה

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

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

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

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

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

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

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

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

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

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

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

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

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

אני רוצה להמליץ בחום שאת ה 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));
  }
}

הערות:

ה (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;
  }
}

הערות:

ה 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));
  }

}

הערות:

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

סיכום

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

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

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

Exit mobile version