Kết hợp tìm nguồn cung ứng sự kiện và hệ thống trạng thái

  • Post category:lập trình


Trong loạt bài gồm hai phần này, đồng nghiệp của tôi Freek và tôi sẽ thảo luận về kiến ​​trúc của dự án mà chúng tôi đang thực hiện. Chúng tôi sẽ chia sẻ những hiểu biết sâu sắc và câu trả lời cho những vấn đề chúng tôi gặp phải trong quá trình thực hiện. Phần này sẽ nói về thiết kế của hệ thống, trong khi phần của Freek sẽ xem xét việc triển khai cụ thể.

Hãy dựng bối cảnh.

Dự án này là một trong những dự án lớn hơn mà chúng tôi đã thực hiện. Cuối cùng, nó sẽ phục vụ hàng trăm nghìn người dùng, xử lý số lượng lớn giao dịch tài chính và các cài đặt độc lập dành riêng cho người thuê sẽ cần được tạo nhanh chóng.

Một yêu cầu quan trọng là quy trình đặt hàng sản phẩm – cốt lõi của hoạt động kinh doanh – có thể được báo cáo dễ dàng cũng như được theo dõi trong suốt lịch sử.

Bên cạnh quy trình khách hàng trực diện này, còn có bảng quản trị phức tạp để quản lý sản phẩm. Trong bối cảnh này, hầu như không cần báo cáo hoặc theo dõi lịch sử hoạt động của quản trị viên; Mục tiêu chính ở đây là có một hệ thống quản lý sản phẩm dễ sử dụng.

Tôi hy vọng bạn hiểu rằng tôi cố tình giữ các thuật ngữ này hơi mơ hồ vì rõ ràng đây không phải là một dự án nguồn mở, mặc dù tôi nghĩ các khái niệm về “quản lý sản phẩm” và “đơn đặt hàng” đủ rõ ràng để bạn hiểu các quyết định thiết kế chúng tôi đã thực hiện.

Trước tiên, hãy thảo luận về cách tiếp cận cách thiết kế hệ thống này dựa trên loạt bài Laravel ngoài CRUD của tôi.

Trong một hệ thống như vậy có thể sẽ có hai nhóm miền: ProductOrdervà hai ứng dụng sử dụng cả hai miền này: một AdminApplication và một CustomerApplication.

Một phiên bản đơn giản hóa sẽ trông giống như thế này:

Đã sử dụng thành công kiến ​​trúc này trong các dự án trước đó, chúng tôi có thể chỉ cần dựa vào nó và kết thúc. Tuy nhiên, nó có một số nhược điểm, đặc biệt đối với dự án mới này: chúng tôi phải lưu ý rằng báo cáo và theo dõi lịch sử là những khía cạnh chính của quy trình đặt hàng. Chúng tôi muốn coi chúng như vậy trong mã của mình chứ không phải chỉ là một tác dụng phụ.

Ví dụ: chúng tôi có thể sử dụng gói nhật ký hoạt động của mình để theo dõi “thông báo lịch sử” về những gì đã xảy ra với đơn đặt hàng. Chúng tôi cũng có thể bắt đầu viết các truy vấn tùy chỉnh trên bảng thứ tự và lịch sử để tạo báo cáo.

Tuy nhiên, những giải pháp này chỉ phát huy tác dụng hiệu quả khi chúng chỉ là những tác dụng phụ nhỏ đối với hoạt động kinh doanh cốt lõi. Trong trường hợp này thì không. Vì vậy, Freek và tôi được giao nhiệm vụ tìm ra một thiết kế cho dự án này để làm cho việc báo cáo và theo dõi lịch sử trở thành một phần cốt lõi, dễ bảo trì và dễ sử dụng của ứng dụng.

Đương nhiên, chúng tôi đã xem xét tìm nguồn cung ứng sự kiện, một giải pháp tuyệt vời và linh hoạt đáp ứng các yêu cầu trên. Tuy nhiên, không có gì miễn phí: việc tìm nguồn cung ứng sự kiện đòi hỏi phải viết khá nhiều mã bổ sung để thực hiện những việc đơn giản khác. Khi bạn thường thực hiện các hành động CRUD đơn giản để thao tác dữ liệu trong cơ sở dữ liệu, giờ đây bạn phải lo lắng về việc gửi các sự kiện, xử lý chúng bằng máy chiếu và lò phản ứng, trong khi luôn lưu ý đến việc lập phiên bản.

Mặc dù rõ ràng rằng hệ thống có nguồn gốc từ sự kiện sẽ giải quyết được nhiều vấn đề nhưng nó cũng sẽ gây ra nhiều chi phí chung, ngay cả ở những nơi nó không mang lại bất kỳ giá trị nào.

Đây là điều tôi muốn nói: nếu chúng ta quyết định lấy nguồn sự kiện Orders mô-đun dựa trên dữ liệu từ Products mô-đun, chúng tôi cũng cần tìm nguồn sự kiện đó, vì nếu không chúng tôi có thể có trạng thái không hợp lệ. Nếu như Products sự kiện không có nguồn gốc và một sự kiện đã bị xóa, chúng tôi không thể xây dựng lại Orders state nữa vì nó thiếu thông tin.

Vì vậy, hoặc chúng tôi tìm nguồn sự kiện mọi thứ hoặc tìm giải pháp cho vấn đề này.

# Nguồn sự kiện tất cả mọi thứ?!

Từ việc tìm hiểu nguồn cung ứng sự kiện trong một số dự án theo sở thích của mình, chúng tôi đã nhận thức sâu sắc rằng chúng tôi không nên đánh giá thấp mức độ phức tạp mà nó tăng thêm. Hơn nữa, Greg Young tuyên bố rằng việc tìm nguồn cung ứng sự kiện cho toàn bộ hệ thống thường là một ý tưởng tồi – anh ấy đã nói rất nhiều về những quan niệm sai lầm về tìm nguồn cung ứng sự kiện và rất đáng xem!

Chúng tôi thấy rõ rằng chúng tôi không muốn lấy nguồn sự kiện của toàn bộ ứng dụng; nó chỉ đơn giản là không có ý nghĩa để làm như vậy. Giải pháp thay thế duy nhất là tìm cách kết hợp một hệ thống có trạng thái với một hệ thống có nguồn sự kiện, nhưng đáng ngạc nhiên là chúng tôi không thể tìm thấy nhiều tài nguyên về chủ đề này.

Tuy nhiên, chúng tôi đã thực hiện một số nghiên cứu tốn nhiều công sức và tìm được câu trả lời cho câu hỏi của mình. Tuy nhiên, câu trả lời không đến từ cộng đồng tìm nguồn cung ứng sự kiện mà đến từ các phương pháp DDD đã được thiết lập tốt: bối cảnh bị giới hạn.

Nếu chúng ta muốn Products module trở thành một hệ thống độc lập, có trạng thái, chúng tôi phải tôn trọng rõ ràng ranh giới giữa ProductsOrders. Thay vì một ứng dụng nguyên khối, chúng tôi sẽ phải coi hai mô-đun này là hai bối cảnh riêng biệt – các dịch vụ riêng biệt, chỉ được phép giao tiếp với nhau theo cách có thể đảm bảo Order context sẽ không bao giờ kết thúc ở trạng thái không hợp lệ.

Nếu Order bối cảnh được xây dựng theo đó nó không dựa vào Product trực tiếp vào bối cảnh, điều đó không thành vấn đề Product bối cảnh đã được xây dựng.

Khi thảo luận vấn đề này với Freek, tôi đã diễn đạt nó như thế này: hãy nghĩ về Products như một dịch vụ riêng biệt, được truy cập thông qua API REST. Làm cách nào để đảm bảo ứng dụng lấy nguồn sự kiện của chúng tôi vẫn hoạt động, ngay cả khi API ngoại tuyến hoặc thực hiện các thay đổi đối với cấu trúc dữ liệu của nó.

Rõ ràng là chúng tôi sẽ không thực sự xây dựng một API để liên lạc giữa các dịch vụ của mình vì chúng sẽ nằm trong cùng một cơ sở mã trên cùng một máy chủ. Tuy nhiên, việc bắt đầu thiết kế hệ thống vẫn là một tư duy tốt.

Ranh giới sẽ như thế này, trong đó mỗi dịch vụ có thiết kế bên trong riêng.

Nếu bạn đọc loạt bài Laravel Beyond CRUD của tôi, bạn đã quen với cách Product bối cảnh hoạt động. Không có gì mới xảy ra ở đó cả. Các Order bối cảnh xứng đáng có thêm một chút thông tin cơ bản.

Nhận thấy một tpyo? Bạn có thể gửi PR để sửa nó. Nếu bạn muốn cập nhật về những gì đang diễn ra trên blog này, bạn có thể Theo dõi danh sách gửi thư của tôi: gửi email đến [email protected] và tôi sẽ thêm bạn vào danh sách.

# Nguồn sự kiện một số phần

Vì vậy, hãy nhìn vào phần nguồn sự kiện. Tôi cho rằng nếu bạn đang đọc bài đăng này thì ít nhất bạn cũng quan tâm đến việc tìm nguồn cung ứng sự kiện, vì vậy tôi sẽ không giải thích chi tiết mọi thứ.

Các OrderAggregateRoot sẽ theo dõi mọi thứ xảy ra trong bối cảnh này và sẽ là điểm khởi đầu để các ứng dụng giao tiếp. Nó cũng sẽ gửi các sự kiện, được lưu trữ và truyền đến tất cả các lò phản ứng và máy chiếu.

Lò phản ứng sẽ xử lý các tác dụng phụ sẽ không bao giờ tái diễn và máy chiếu sẽ thực hiện các phép chiếu. Trong trường hợp của chúng tôi đây là những mô hình Laravel đơn giản. Những mô hình này có thể được đọc từ bất kỳ bối cảnh nào khác, mặc dù chúng chỉ có thể được ghi từ bên trong máy chiếu.

Một quyết định thiết kế mà chúng tôi đã đưa ra ở đây là không phân chia các mô hình đọc và viết của mình, hiện tại chúng tôi dựa vào mô hình nói quy ước bằng văn bản rằng những mô hình này chỉ được ghi thông qua máy chiếu của chúng. Một ví dụ về mô hình chiếu như vậy sẽ là một Order.

Nguyên tắc quan trọng nhất cần nhớ là toàn bộ trạng thái của Order bối cảnh chỉ có thể được xây dựng lại từ các sự kiện được lưu trữ của nó.

Vậy làm cách nào để chúng tôi lấy dữ liệu từ các bối cảnh khác? Làm sao có thể Order bối cảnh được thông báo khi có điều gì đó xảy ra trong Product bối cảnh có liên quan đến nó? Một điều chắc chắn là: tất cả các thông tin liên quan về Products sẽ cần được lưu trữ dưới dạng các sự kiện trong Order bối cảnh; vì trong bối cảnh đó, các sự kiện là nguồn gốc duy nhất của sự thật.

Để đạt được điều này, chúng tôi đã giới thiệu loại trình xử lý sự kiện thứ ba. Đã có máy chiếu và lò phản ứng; bây giờ chúng tôi thêm khái niệm về người đăng ký. Những người đăng ký này được phép nghe các sự kiện từ các bối cảnh khác và xử lý chúng phù hợp với bối cảnh hiện tại của họ. Rất có thể, họ hầu như sẽ luôn chuyển đổi các sự kiện bên ngoài thành các sự kiện nội bộ, được lưu trữ.

Từ thời điểm các sự kiện được lưu trữ trong Order bối cảnh, chúng ta có thể yên tâm quên đi bất kỳ sự phụ thuộc nào vào Product bối cảnh.

Một số độc giả có thể nghĩ rằng chúng tôi đang sao chép dữ liệu bằng cách sao chép các sự kiện giữa hai bối cảnh này. Tất nhiên chúng tôi đang lưu trữ một Orders sự kiện cụ thể, dựa trên thời điểm Product đã từng là created, vì vậy, một số dữ liệu sẽ được sao chép. Tuy nhiên, việc này mang lại nhiều lợi ích hơn bạn nghĩ.

Trước hết: việc Product context không cần biết bất cứ điều gì về bối cảnh khác sẽ sử dụng dữ liệu của nó. Nó không cần phải tính đến phiên bản sự kiện vì các sự kiện của nó sẽ không bao giờ được lưu trữ. Điều này cho phép chúng ta làm việc trong Product bối cảnh như thể nó là bất kỳ ứng dụng có trạng thái, bình thường nào mà không cần thêm nguồn sự kiện phức tạp.

Thứ hai: sẽ có nhiều thứ hơn là chỉ Order bối cảnh có nguồn gốc từ sự kiện và tất cả các bối cảnh này có thể lắng nghe riêng lẻ các sự kiện có liên quan được kích hoạt trong Product bối cảnh.

Và thứ ba: chúng tôi không phải lưu trữ bản sao đầy đủ của bản gốc Product các sự kiện vì mỗi ngữ cảnh có thể chọn và lưu trữ dữ liệu phù hợp với trường hợp sử dụng của chính nó.

# Di chuyển dữ liệu thì sao?

Một câu hỏi mới nảy sinh.

Giả sử hệ thống này đã được sản xuất được một năm và chúng tôi quyết định thêm một bối cảnh mới lấy nguồn từ sự kiện; một việc cũng đòi hỏi kiến ​​thức về Product bối cảnh. Bản gốc Product các sự kiện không được lưu trữ – vì những lý do được liệt kê ở trên – vậy làm cách nào chúng tôi có thể xây dựng trạng thái ban đầu cho bối cảnh mới của mình?

Câu trả lời là: tại thời điểm triển khai, chúng tôi sẽ phải đọc tất cả dữ liệu sản phẩm và gửi các sự kiện có liên quan đến ngữ cảnh mới được thêm vào, dựa trên các sản phẩm hiện có. Việc di chuyển một lần này là một chi phí bổ sung, mặc dù nó mang lại cho chúng tôi sự tự do làm việc trong Product bối cảnh mà không bao giờ phải lo lắng về bên ngoài. Đối với dự án này đó là một cái giá đáng phải trả.

# Tích hợp cuối cùng

Cuối cùng, chúng tôi có thể sử dụng dữ liệu trong các ứng dụng được thu thập từ tất cả các ngữ cảnh bằng cách sử dụng các mô hình chỉ đọc. Một lần nữa, trong trường hợp của chúng tôi và tính đến thời điểm hiện tại, các mô hình này ở chế độ chỉ đọc theo quy ước; chúng ta có thể thay đổi điều đó trong tương lai.

Truyền thông từ ứng dụng đến Product context được thực hiện giống như bất kỳ ứng dụng trạng thái thông thường nào sẽ làm. Giao tiếp giữa các ứng dụng và bối cảnh có nguồn gốc sự kiện, chẳng hạn như Orders được thực hiện thông qua gốc tổng hợp của nó.

Bây giờ, đây là cái nhìn tổng quan cuối cùng. Một số mũi tên vẫn còn thiếu trong sơ đồ này, nhưng tôi hy vọng rằng luồng liên quan giữa và bên trong các bối cảnh và ứng dụng là rõ ràng.


Chìa khóa để giải quyết vấn đề của chúng tôi là xem xét bối cảnh bị giới hạn của DDD. Chúng mô tả các ranh giới nghiêm ngặt trong cơ sở mã của chúng tôi – những ranh giới mà chúng tôi không thể đơn giản vượt qua bất cứ khi nào chúng tôi muốn. Chắc chắn điều này sẽ tăng thêm độ phức tạp, mặc dù nó cũng mang lại sự tự do để xây dựng từng bối cảnh theo bất kỳ cách nào chúng ta muốn mà không phải lo lắng về việc hỗ trợ người khác.

Phần cuối cùng của câu đố là chỉ dựa vào các sự kiện như một phương tiện giao tiếp giữa các bối cảnh. Một lần nữa, nó tạo thêm một lớp phức tạp nhưng cũng là một phương tiện tách rời và linh hoạt.

Bây giờ là lúc tìm hiểu sâu hơn về cách chúng tôi lập trình điều này trong dự án Laravel. Đây là đồng nghiệp Freek của tôi với phần hai.



Trả lời