CQRS ומקורות אירועים בג'אווה

1. הקדמה

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

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

2. מושגי יסוד

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

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

2.1. מקורות אירועים

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

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

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

2.2. CQRS

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

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

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

3. יישום פשוט

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

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

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

בתהליך ננצל כמה מהמושגים מהעיצוב מונע תחום (DDD) בדוגמה שלנו.

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

3.1. סקירה כללית של היישום

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

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

3.2. יישום יישום

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

מחלקה ציבורית משתמש {פרטי מחרוזת userid; פרטי מחרוזת firstName; שם משפחה פרטי מחרוזת; פרטי קביעת אנשי קשר; כתובות קבוצות פרטיות; // getters and setters} class public Contact {private סוג מחרוזת; פרט מחרוזת פרטי; // getters and setters} class class Address {private String City; מדינת מיתרים פרטית; מיקוד מחרוזת פרטי; // גטרים וקובעים}

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

מחלקה ציבורית UserRepository {חנות מפות פרטית = HashMap חדש (); }

כעת נגדיר שירות לחשיפת פעולות CRUD טיפוסיות במודל התחום שלנו:

מחלקה ציבורית UserService {מאגר UserRepository פרטי; שירות משתמש ציבורי (מאגר UserRepository) {this.repository = מאגר; } create public User ריק (מחרוזת userId, שם מחרוזת, שם משפחה מחרוזת) {משתמש משתמש = משתמש חדש (userId, שם פרטי, שם משפחה); repository.addUser (userId, user); } עדכון משתמש ריק (מחרוזת userId, הגדר אנשי קשר, הגדר כתובות) {User user = repository.getUser (userId); user.setContacts (אנשי קשר); user.setAddresses (כתובות); repository.addUser (userId, user); } ציבורי הגדר getContactByType (מחרוזת userId, מחרוזת contactType) {משתמש משתמש = repository.getUser (userId); הגדר אנשי קשר = user.getContacts (); להחזיר אנשי קשר.זרם () .פילטר (c -> c.getType (). שווה (contactType)) .collect (Collectors.toSet ()); } ציבורי הגדר getAddressByRegion (מחרוזת userId, מחרוזת מצב) {User user = repository.getUser (userId); הגדר כתובות = user.getAddresses (); להחזיר את הכתובות.זרם () .פילטר (a -> a.getState (). שווה (מצב)) .collect (Collectors.toSet ()); }}

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

3.3. בעיות ביישום זה

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

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

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

4. הכנסת CQRS

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

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

  • צבירה / צבירה:

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

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

  • הקרנה / מקרן:

הקרנה היא דפוס חשוב נוסף המועיל מאוד ל- CQRS. הַקרָנָה פירושו בעצם ייצוג אובייקטים של תחומים בצורות ומבנים שונים.

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

4.1. יישום צד כתיבה של היישום

בואו וניישם תחילה את צד הכתיבה של היישום.

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

בואו נראה את הפקודות שלנו:

מחלקה ציבורית CreateUserCommand {private String userId; פרטי מחרוזת firstName; שם משפחה פרטי מחרוזת; } UpdateUserCommand המחלקה הציבורית {userId מחרוזת פרטית; כתובות קבוצות פרטיות; פרטי קביעת אנשי קשר; }

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

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

מחלקה ציבורית UserAggregate {פרטי UserWriteRepository writeRepository; UserAggregate ציבורי (מאגר UserWriteRepository) {this.writeRepository = מאגר; } משתמש ציבורי handleCreateUserCommand (פקודת CreateUserCommand) {משתמש משתמש = משתמש חדש (command.getUserId (), command.getFirstName (), command.getLastName ()); writeRepository.addUser (user.getUserid (), user); משתמש חוזר; } משתמש ציבורי handleUpdateUserCommand (פקודת UpdateUserCommand) {משתמש משתמש = writeRepository.getUser (command.getUserId ()); user.setAddresses (command.getAddresses ()); user.setContacts (command.getContacts ()); writeRepository.addUser (user.getUserid (), user); משתמש חוזר; }}

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

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

מחלקה ציבורית UserWriteRepository {חנות מפות פרטית = HashMap חדש (); // אביזרים ומוטציות}

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

4.2. יישום צד הקריאה של היישום

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

מחלקה ציבורית UserAddress {מפה פרטית addressByRegion = HashMap חדש (); } UserContact בכיתה ציבורית {מפה פרטית contactByType = HashMap חדש (); }

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

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

מחלקה ציבורית UserReadRepository {private map userAddress = HashMap חדש (); משתמש פרטי במפהContact = HashMap חדש (); // אביזרים ומוטציות}

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

בואו נראה את השאלות שלנו:

מחלקה ציבורית ContactByTypeQuery {private String userId; פרטי מחרוזת contactType; } class class AddressByRegionQuery {userId מחרוזת פרטית; מדינת מיתרים פרטית; }

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

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

מחלקה ציבורית UserProjection {פרטי UserReadRepository readRepository; UserProjection ציבורי (UserReadRepository readRepository) {this.readRepository = readRepository; } ידית קבוצה ציבורית (שאילתת ContactByTypeQuery) {UserContact userContact = readRepository.getUserContact (query.getUserId ()); להחזיר userContact.getContactByType () .get (query.getContactType ()); } ידית קבוצה ציבורית (שאילתת AddressByRegionQuery) {UserAddress userAddress = readRepository.getUserAddress (query.getUserId ()); להחזיר userAddress.getAddressByRegion () .get (query.getState ()); }}

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

4.3. סנכרון נתוני קריאה וכתיבה

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

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

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

מחלקה ציבורית UserProjector {UserReadRepository readRepository = UserReadRepository חדש (); UserProjector ציבורי (UserReadRepository readRepository) {this.readRepository = readRepository; } פרויקט ריק ריק (משתמש משתמש) {UserContact userContact = Optional.ofNullable (readRepository.getUserContact (user.getUserid ())) .orElse (UserContact חדש ()); מַפָּה contactByType = חדש HashMap (); עבור (Contact contact: user.getContacts ()) {Set contacts = Optional.ofNullable (contactByType.get (contact.getType ())) .orElse (חדש HashSet ()); contacts.add (קשר); contactByType.put (contact.getType (), אנשי קשר); } userContact.setContactByType (contactByType); readRepository.addUserContact (user.getUserid (), userContact); UserAddress userAddress = Optional.ofNullable (readRepository.getUserAddress (user.getUserid ())). OrElse (UserAddress חדש ()); מַפָּה addressByRegion = HashMap חדש (); עבור (כתובת כתובת: user.getAddresses ()) {הגדר כתובות = Optional.ofNullable (addressByRegion.get (address.getState ())) .orElse (חדש HashSet ()); adressen.add (כתובת); addressByRegion.put (address.getState (), כתובות); } userAddress.setAddressByRegion (addressByRegion); readRepository.addUserAddress (user.getUserid (), userAddress); }}

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

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

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

4.4. יתרונות וחסרונות של CQRS

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

בואו נדון כעת בכמה מהיתרונות האחרים ש- CQRS מביא לארכיטקטורת יישומים:

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

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

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

5. הצגת המקור לאירועים

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

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

בואו נראה איך זה משנה את המאגר שלנו:

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

5.1. יישום אירועים וחנות אירועים

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

אירוע בכיתה מופשטת ציבורית {גמר ציבורי UUID id = UUID.randomUUID (); גמר ציבורי תאריך יצירה = תאריך חדש (); }

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

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

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

מחלקה ציבורית UserCreatedEvent מאריך אירוע {פרטי מחרוזת userId; פרטי מחרוזת firstName; שם משפחה פרטי מחרוזת; } מחלקה ציבורית UserContactAddedEvent מרחיב את האירוע {private String contactType; פרטי מחרוזת contactDetails; } מחלקה ציבורית UserContactRemovedEvent מרחיב אירוע {private String contactType; פרטי מחרוזת contactDetails; } מחלקה ציבורית UserAddressAddedEvent מאריך אירוע {עיר מחרוזת פרטית; מדינת מיתרים פרטית; פרטי מחרוזת postCode; } מחלקה ציבורית UserAddressRemovedEvent מאריך אירוע {עיר מחרוזת פרטית; מדינת מיתרים פרטית; פרטי מחרוזת postCode; }

אלה POJO פשוטים ב- Java המכילים את פרטי אירוע הדומיין. עם זאת, הדבר החשוב לציין כאן הוא פירוט האירועים.

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

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

מעמד ציבורי EventStore {מפה פרטית חנות = HashMap חדש (); }

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

5.2. הפקת וצריכת אירועים

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

בואו נראה איך נוכל להשיג זאת:

מחלקת ציבור UserService {מאגר פרטי EventStore; UserService ציבורי (מאגר EventStore) {this.repository = מאגר; } createUser public void (String userId, String firstName, String lastName) {repository.addEvent (userId, UserCreatedEvent new (userId, firstName, lastName)); } עדכון הריק הציבורי User (מחרוזת userId, הגדר אנשי קשר, הגדר כתובות) {User user = UserUtility.recreateUserState (מאגר, userId); user.getContacts (). stream () .filter (c ->! contacts.contains (c)) .forEach (c -> repository.addEvent (userId, UserContactRemovedEvent new (c.getType (), c.getDetail ()) )); contacts.stream () .filter (c ->! user.getContacts (). מכיל (c)). forEach (c -> repository.addEvent (userId, UserContactAddedEvent חדש (c.getType (), c.getDetail ()) )); user.getAddresses (). stream () .filter (a ->! addresses.contains (a)) .forEach (a -> repository.addEvent (userId, UserAddressRemovedEvent new (a.getCity (), a.getState (), a.getPostcode ()))); adressen.stream () .filter (a ->! user.getAddresses (). מכיל (a)). forEach (a -> repository.addEvent (userId, UserAddressAddedEvent חדש (a.getCity (), a.getState (), a.getPostcode ()))); } ציבורי הגדר getContactByType (מחרוזת userId, מחרוזת contactType) {משתמש user = UserUtility.recreateUserState (מאגר, userId); להחזיר user.getContacts (). stream () .filter (c -> c.getType (). שווה ל- (contactType)) .collect (Collectors.toSet ()); } ציבורי להגדיר getAddressByRegion (מחרוזת userId, מחרוזת מצב) זורק חריג {משתמש user = UserUtility.recreateUserState (מאגר, userId); להחזיר user.getAddresses (). stream () .filter (a -> a.getState (). שווה (state)) .collect (Collectors.toSet ()); }}

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

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

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

5.3. יתרונות וחסרונות ממקורות אירועים

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

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

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

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

6. CQRS עם מקורות אירועים

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

ראשית נראה כיצד ארכיטקטורת היישומים מפגישה אותם:

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

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

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

6.1. קירוב CQRS ומקורות אירועים

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

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

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

מחלקה ציבורית UserAggregate {private EventStore writeRepository; UserAggregate ציבורי (מאגר EventStore) {this.writeRepository = מאגר; } רשימת ציבורים handleCreateUserCommand (פקודת CreateUserCommand) {אירוע UserCreatedEvent = UserCreatedEvent חדש (command.getUserId (), command.getFirstName (), command.getLastName ()); writeRepository.addEvent (command.getUserId (), אירוע); החזר Arrays.asList (אירוע); } רשימת ציבורי handleUpdateUserCommand (פקודת UpdateUserCommand) {משתמש משתמש = UserUtility.recreateUserState (writeRepository, command.getUserId ()); רשימת אירועים = ArrayList חדש (); רשימת אנשי קשר TooRemove = user.getContacts (). Stream () .filter (c ->! Command.getContacts (). מכיל (c)) .collect (Collectors.toList ()); עבור (Contact contact: contactsToRemove) {UserContactRemovedEvent contactRemovedEvent = UserContactRemovedEvent חדש (contact.getType (), contact.getDetail ()); events.add (contactRemovedEvent); writeRepository.addEvent (command.getUserId (), contactRemovedEvent); } רשימת אנשי קשר ToAdd = command.getContacts (). Stream () .filter (c ->! User.getContacts (). מכיל (c)) .collect (Collectors.toList ()); עבור (Contact contact: contactsToAdd) {UserContactAddedEvent contactAddedEvent = UserContactAddedEvent חדש (contact.getType (), contact.getDetail ()); events.add (contactAddedEvent); writeRepository.addEvent (command.getUserId (), contactAddedEvent); } // עיבוד דומה של כתובות לכתובת להסרה // עיבוד של כתובות כתובת דומה להוספת אירועי החזרה; }}

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

מחלקה ציבורית UserProjector {UserReadRepository readRepository = UserReadRepository חדש (); UserProjector ציבורי (UserReadRepository readRepository) {this.readRepository = readRepository; } פרויקט חלל ציבורי (מחרוזת userId, אירועי רשימה) {עבור (אירוע אירוע: אירועים) {if (event event of UserAddressAddedEvent) להחיל (userId, (UserAddressAddedEvent) אירוע); אם (אירוע מופע של UserAddressRemovedEvent) חל (אירוע userId, (אירוע UserAddressRemovedEvent)); אם (מופע אירוע של UserContactAddedEvent) חל (אירוע userId, (אירוע UserContactAddedEvent)); אם (מופע אירוע של UserContactRemovedEvent) להחיל (userId, (אירוע UserContactRemovedEvent)); }} חל חלל ציבורי (מחרוזת userId, אירוע UserAddressAddedEvent) {כתובת כתובת = כתובת חדשה (event.getCity (), event.getState (), event.getPostCode ()); UserAddress userAddress = Optional.ofNullable (readRepository.getUserAddress (userId)) .orElse (UserAddress חדש ()); הגדר כתובות = Optional.ofNullable (userAddress.getAddressByRegion () .get (address.getState ())). OrElse (חדש HashSet ()); adressen.add (כתובת); userAddress.getAddressByRegion () .put (address.getState (), כתובות); readRepository.addUserAddress (userId, userAddress); } חל חלל ציבורי (מחרוזת userId, UserAddressRemovedEvent אירוע) {כתובת כתובת = כתובת חדשה (event.getCity (), event.getState (), event.getPostCode ()); UserAddress userAddress = readRepository.getUserAddress (userId); אם (userAddress! = null) {הגדר כתובות = userAddress.getAddressByRegion () .get (address.getState ()); אם (כתובות! = אפס) כתובות.הסר (כתובת); readRepository.addUserAddress (userId, userAddress); }} חל חלל ציבורי (מחרוזת userId, אירוע UserContactAddedEvent) {// מטפל באופן דומה באירוע UserContactAddedEvent} חל חלל ציבורי (מחרוזת userId, אירוע UserContactRemovedEvent) {// מטפל באופן דומה באירוע UserContactRemovedEvent}}

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

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

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

7. מסקנה

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

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

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

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


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