קבוצות DDD מתמשכות

1. סקירה כללית

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

2. מבוא לצבירות

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

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

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

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

2.1. דוגמה להזמנת רכש

אז נניח שאנחנו רוצים לעצב הזמנת רכש:

סדר הזמנה {שורות סדר איסוף פרטי; סכום פרטי פרטי TotalCost; // ...}
class OrderLine {מוצר מוצר פרטי; כמות אינטנס פרטית; // ...}
מוצר קלאסי {מחיר כסף פרטי; // ...}

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

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

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

2.2. תכנון מצטבר נאיבי

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

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

הזמנת הזמנה = הזמנה חדשה (); order.setOrderLines (Arrays.asList (orderLine0, orderLine1)); order.setTotalCost (Money.zero (CurrencyUnit.USD)); // זה לא נראה טוב ...

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

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

2.3. שורש מצטבר

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

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

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

סדר הזמנה {פרטית סופית רשימת סדר שורות; סכום פרטי פרטי totalCost; הזמנה (רשימת שורות סדר) {checkNotNull (שורות הזמנה); אם (orderLines.isEmpty ()) {זרוק IllegalArgumentException חדש ("הזמנה חייבת לכלול לפחות שורת הזמנה אחת"); } this.orderLines = ArrayList חדש (סדר שורות); totalCost = calcTotalCost (); } בטל addLineItem (OrderLine orderLine) {checkNotNull (orderLine); orderLines.add (orderLine); totalCost = totalCost.plus (orderLine.cost ()); } בטל removeLineItem (int שורה) {OrderLine removedLine = orderLines.remove (שורה); totalCost = totalCost.minus (removedLine.cost ()); } כסף totalCost () {return totalCost; } // ...}

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

כפי שאנו רואים, מדובר במצטבר די פשוט.

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

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

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

3. JPA ו- Hibernate

בקטע זה, בואו ננסה להתמיד ב להזמין צבירה באמצעות JPA ו- Hibernate. נשתמש באתחול האביב ובמתנע JPA:

 org.springframework.boot spring-boot-starter-data-jpa 

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

כנראה הבעיה הגדולה ביותר בעבודה עם מסגרות ORM היא הפשטות של עיצוב המודל שלנו. לפעמים זה מכונה גם אי התאמה בין עכבה לאובייקט. בואו נחשוב מה יקרה אם נרצה להתמיד בזה להזמין לְקַבֵּץ:

@DisplayName ("הזמנה נתונה עם שני פריטי שורה, כאשר היא נמשכת ואז הסדר נשמר") @Test מבט הריק הציבורי () זורק חריג {// נתון סדר JpaOrder = preparTestOrderWithTwoLineItems (); // כאשר JpaOrder להציל את ההזמנה = repository.save (סדר); // ואז JpaOrder foundOrder = repository.findById (savedOrder.getId ()) .get (); assertThat (foundOrder.getOrderLines ()). hasSize (2); }

בשלב זה, מבחן זה ישליך על יוצא מן הכלל: java.lang.IllegalArgumentException: ישות לא ידועה: com.baeldung.ddd.order.Order. ברור שחסרים לנו כמה מדרישות ה- JPA:

  1. הוסף הערות מיפוי
  2. OrderLine ו מוצר שיעורים חייבים להיות ישויות או @ ניתן להטמיעה מחלקות, אובייקטים ערכיים לא פשוטים
  3. הוסף קונסטרוקטור ריק לכל ישות או @ ניתן להטמיעה מעמד
  4. החלף כֶּסֶף נכסים עם סוגים פשוטים

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

3.1. שינויים בחפצי הערך

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

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

אפשר להשתמש @ משובץ ו @ElementCollection ביאורים במקום זאת, אך גישה זו עלולה לסבך את העניינים הרבה בעת שימוש בגרף אובייקטים מורכב (למשל @ ניתן להטמיעה לאובייקט שיש אחר @ משובץ רכוש וכו ').

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

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

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

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

3.2. סוגים מורכבים

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

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

במקרה כזה, אנו עשויים בסופו של דבר לכתוב סוג מותאם אישית @מֵמִיר זמין מ- JPA 2.1. זה עשוי לדרוש קצת עבודה נוספת.

לחלופין, אנו יכולים גם לפצל את כֶּסֶף נכס לשני נכסים בסיסיים. לדוגמה חוּט ליחידת מטבע ו BigDecimal עבור הערך בפועל.

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

3.3. סיכום

למרות ש- JPA הוא אחד המפרטים המאומצים ביותר בעולם, יתכן שזו לא האפשרות הטובה ביותר להתמיד אצלנו להזמין לְקַבֵּץ.

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

בעיקרון, יש לנו כאן שלוש אפשרויות:

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

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

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

4. חנות מסמכים

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

לצרכי הדרכה זו, נתמקד במסמכים דמויי JSON.

בואו נסתכל מקרוב על איך נראית בעיית התמדה ההזמנה שלנו בחנות מסמכים כמו MongoDB.

4.1. המשך צבירה באמצעות MongoDB

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

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

לפני שנמשיך, בואו נוסיף את המתנע של Spring Boot MongoDB:

 org.springframework.boot spring-boot-starter-data-mongodb 

כעת אנו יכולים להריץ מקרה בדיקה דומה כמו בדוגמת JPA, אך הפעם באמצעות MongoDB:

@DisplayName ("הזמנה נתונה עם שני פריטי שורה, כאשר הם ממשיכים להשתמש במאגר המונגו ואז הסדר נשמר") @Test void test () זורק חריג {// נתון Order Order = preparTestOrderWithTwoLineItems (); // כאשר repo.save (סדר); // ואז List foundOrders = repo.findAll (); assertThat (foundOrders) .hasSize (1); רשימה findOrderLines = foundOrders.iterator (). הבא () .getOrderLines (); assertThat (foundOrderLines) .hasSize (2); assertThat (foundOrderLines) .containsOnlyElementsOf (order.getOrderLines ()); }

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

והנה מה שלנו להזמין צבירה מופיעה בחנות:

{"_id": ObjectId ("5bd8535c81c04529f54acd14"), "orderLines": [{"product": {"price": {"money": {"currency": {"code": "USD", "numericCode": 840, "decimalPlaces": 2}, "סכום": "10.00"}}}, "כמות": 2}, {"מוצר": {"מחיר": {"כסף": {"מטבע": {"קוד ":" USD "," numericCode ": 840," decimalPlaces ": 2}," סכום ":" 5.00 "}}}," כמות ": 10}]," totalCost ": {" money ": {" currency ": {" code ":" USD "," numericCode ": 840," decimalPlaces ": 2}," amount ":" 70.00 "}}," _class ":" com.baeldung.ddd.order.mongo.Order "}

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

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

4.2. סיכום

התמדת אגרגטים באמצעות MongoDB היא פשוטה יותר משימוש ב- JPA.

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

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

5. מסקנה

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

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

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

קוד המקור המלא של כל הדוגמאות זמין באתר GitHub.


$config[zx-auto] not found$config[zx-overlay] not found