Làm thế nào chính xác một lệnh CQRS nên được xác nhận và chuyển đổi thành một đối tượng miền?


22

Tôi đã điều chỉnh CQRS 1 của người nghèo từ khá lâu rồi vì tôi thích sự linh hoạt của nó để có dữ liệu dạng hạt trong một kho dữ liệu, cung cấp khả năng phân tích tuyệt vời và do đó tăng giá trị doanh nghiệp và khi cần một dữ liệu khác có chứa dữ liệu không chuẩn hóa để tăng hiệu suất .

Nhưng thật không may, ngay từ đầu tôi đã phải vật lộn với vấn đề chính xác là tôi nên đặt logic kinh doanh trong loại kiến ​​trúc này.

Theo những gì tôi hiểu, một lệnh là một phương tiện để truyền đạt ý định và bản thân nó không có quan hệ với một miền. Chúng cơ bản là dữ liệu (câm - nếu bạn muốn) chuyển đối tượng. Điều này là để làm cho các lệnh dễ dàng chuyển giữa các công nghệ khác nhau. Áp dụng tương tự cho các sự kiện như phản ứng với các sự kiện hoàn thành thành công.

Trong một ứng dụng DDD điển hình, logic nghiệp vụ nằm trong các thực thể, các đối tượng giá trị, gốc tổng hợp, chúng rất phong phú về cả dữ liệu cũng như hành vi. Nhưng một lệnh không phải là một đối tượng miền, do đó không nên giới hạn trong việc biểu diễn dữ liệu miền, vì điều đó gây quá nhiều căng thẳng cho chúng.

Vì vậy, câu hỏi thực sự là: logic chính xác ở đâu?

Tôi đã phát hiện ra rằng tôi có xu hướng đối mặt với cuộc đấu tranh này thường xuyên nhất khi cố gắng xây dựng một tổng hợp khá phức tạp, đặt ra một số quy tắc về sự kết hợp các giá trị của nó. Ngoài ra, khi mô hình hóa các đối tượng miền tôi muốn theo mô hình không nhanh , biết khi nào một đối tượng đạt đến một phương thức thì nó ở trạng thái hợp lệ.

Giả Carsử tổng hợp sử dụng hai thành phần:

  • Transmission,
  • Engine.

Cả hai TransmissionEngineđối tượng giá trị được biểu diễn như các loại siêu và có theo loại phụ, AutomaticManualđược truyền đi, hay PetrolElectricđộng cơ tương ứng.

Trong miền này, việc tự mình tạo thành công Transmission, có thể Automatichoặc Manualhoặc một trong hai loại Enginelà hoàn toàn tốt. Nhưng Cartổng hợp giới thiệu một vài quy tắc mới, chỉ áp dụng khi TransmissionEnginecác đối tượng được sử dụng trong cùng một bối cảnh. Cụ thể là:

  • Khi một chiếc xe sử dụng Electricđộng cơ, loại truyền động duy nhất được phép là Automatic.
  • Khi một chiếc xe sử dụng Petrolđộng cơ, nó có thể có một trong hai loại Transmission.

Tôi có thể bắt vi phạm kết hợp thành phần này ở cấp độ tạo lệnh, nhưng như tôi đã nói trước đây, từ những gì tôi hiểu rằng không nên thực hiện vì lệnh sau đó sẽ chứa logic nghiệp vụ nên được giới hạn trong lớp miền.

Một trong các tùy chọn là di chuyển xác thực logic nghiệp vụ này sang lệnh của trình xác nhận hợp lệ, nhưng điều này dường như cũng không đúng. Cảm giác như tôi sẽ giải mã lệnh, kiểm tra các thuộc tính của nó được lấy bằng cách sử dụng getters và so sánh chúng trong trình xác nhận và kiểm tra kết quả. Điều đó hét lên như một sự vi phạm luật pháp của Demeter đối với tôi.

Loại bỏ tùy chọn xác nhận được đề cập bởi vì nó có vẻ không khả thi, có vẻ như người ta nên sử dụng lệnh và xây dựng tổng hợp từ nó. Nhưng logic này nên tồn tại ở đâu? Nó có nên nằm trong bộ xử lý lệnh chịu trách nhiệm xử lý một lệnh cụ thể không? Hoặc có lẽ nó nên nằm trong trình xác nhận lệnh (tôi cũng không thích cách tiếp cận này)?

Tôi hiện đang sử dụng một lệnh và tạo tổng hợp từ nó trong trình xử lý lệnh có trách nhiệm. Nhưng khi tôi làm điều này, nếu tôi có một trình xác nhận lệnh thì nó sẽ không chứa bất cứ thứ gì cả, bởi vì nếu CreateCarlệnh tồn tại thì nó sẽ chứa các thành phần mà tôi biết là hợp lệ trong các trường hợp riêng biệt nhưng tổng hợp có thể nói khác nhau.


Chúng ta hãy tưởng tượng một kịch bản khác nhau trộn các quá trình xác nhận khác nhau - tạo một người dùng mới bằng cách sử dụng một CreateUserlệnh.

Lệnh chứa Idmột người dùng sẽ được tạo và của họ Email.

Hệ thống nêu các quy tắc sau cho địa chỉ email của người dùng:

  • phải là duy nhất,
  • không được để trống
  • phải có tối đa 100 ký tự (độ dài tối đa của cột db).

Trong trường hợp này, mặc dù có một email duy nhất là một quy tắc kinh doanh, việc kiểm tra nó trong một tổng hợp có rất ít ý nghĩa, bởi vì tôi sẽ cần phải tải toàn bộ bộ email hiện tại trong hệ thống vào bộ nhớ và kiểm tra email trong lệnh chống lại tổng hợp ( Eeeek! Something, Something, Performance.). Do đó, tôi sẽ chuyển kiểm tra này sang trình xác nhận lệnh, sẽ sử dụng UserRepositorynhư một phụ thuộc và sử dụng kho lưu trữ để kiểm tra xem người dùng có email có trong lệnh đã tồn tại hay không.

Khi nói đến điều này, đột nhiên có ý nghĩa để đặt hai quy tắc email khác trong trình xác nhận lệnh. Nhưng tôi có cảm giác các quy tắc nên thực sự được trình bày trong một Usertổng hợp và trình xác nhận lệnh chỉ nên kiểm tra tính duy nhất và nếu xác thực thành công, tôi nên tiến hành tạo Usertổng hợp trong CreateUserCommandHandlervà chuyển nó vào kho lưu trữ để lưu.

Tôi cảm thấy như vậy bởi vì phương thức lưu của kho lưu trữ có khả năng chấp nhận một tổng hợp đảm bảo rằng một khi tổng hợp được thông qua, tất cả các bất biến đều được đáp ứng. Khi logic (ví dụ như không trống rỗng) chỉ xuất hiện trong chính xác thực lệnh, một lập trình viên khác hoàn toàn có thể bỏ qua xác thực này và gọi phương thức lưu trong UserRepositorymột Userđối tượng trực tiếp có thể dẫn đến lỗi cơ sở dữ liệu nghiêm trọng, vì email có thể có đã quá lâu.

Làm thế nào để cá nhân bạn xử lý các xác nhận và biến đổi phức tạp? Tôi chủ yếu hài lòng với giải pháp của mình, nhưng tôi cảm thấy mình cần khẳng định rằng ý tưởng và cách tiếp cận của tôi không hoàn toàn ngu ngốc để khá hài lòng với các lựa chọn. Tôi hoàn toàn cởi mở với những cách tiếp cận hoàn toàn khác nhau. Nếu bạn có một cái gì đó mà cá nhân bạn đã cố gắng và làm việc rất tốt cho bạn, tôi rất muốn thấy giải pháp của bạn.


1 Làm việc như một nhà phát triển PHP chịu trách nhiệm tạo các hệ thống RESTful, việc giải thích CQRS của tôi làm lệch đi một chút so với cách tiếp cận xử lý lệnh async tiêu chuẩn , chẳng hạn như đôi khi trả về kết quả từ các lệnh do cần xử lý đồng bộ các lệnh.


Tôi cần một số mã ví dụ. các đối tượng lệnh của bạn trông như thế nào và bạn tạo chúng ở đâu?
Ewan

@Ewan Tôi sẽ thêm các mẫu mã vào ngày hôm nay hoặc ngày mai. Để lại cho một chuyến đi trong vài phút.
Andy

Là một lập trình viên PHP, tôi khuyên bạn nên xem triển khai CQRS + ES của mình: github.com/xprt64/cqrs-es
Constantin Galbenu

@ConstantinGALBENU Chúng ta có nên coi cách giải thích CQRS của Greg Young là đúng (mà chúng ta có lẽ nên làm) thì sự hiểu biết về CQRS của bạn là sai - hoặc ít nhất là việc triển khai PHP của bạn là. Các lệnh không được xử lý bởi các tổng hợp trực tiếp. Các lệnh sẽ được xử lý bởi các trình xử lý lệnh có thể tạo ra các thay đổi trong các tập hợp mà sau đó tạo ra các sự kiện sẽ được sử dụng để sao chép trạng thái.
Andy

Tôi không nghĩ cách giải thích của chúng tôi là khác nhau. Bạn chỉ cần đào sâu hơn vào DDD (ở cấp chiến thuật của Uẩn) hoặc mở to mắt hơn. Có ít nhất hai phong cách thực hiện CQRS. Tôi sử dụng một trong số họ. Việc triển khai của tôi giống với mô hình Actor hơn và làm cho lớp Ứng dụng rất mỏng, đây luôn là một điều tốt. Tôi quan sát thấy có rất nhiều mã trùng lặp bên trong các dịch vụ ứng dụng đó và quyết định thay thế chúng bằng a CommandDispatcher.
Constantin Galbenu

Câu trả lời:


22

Câu trả lời sau đây là trong bối cảnh của kiểu CQRS được quảng bá bởi cqrs.nu trong đó các lệnh đến trực tiếp trên các tập hợp. Trong kiểu kiến ​​trúc này, các dịch vụ ứng dụng đang được thay thế bởi một thành phần cơ sở hạ tầng ( CommandDispatcher ) xác định tổng hợp, tải nó, gửi lệnh đó và sau đó duy trì tổng hợp (như một chuỗi các sự kiện nếu sử dụng nguồn sự kiện).

Vì vậy, câu hỏi thực sự là: logic chính xác ở đâu?

Có nhiều loại logic (xác nhận). Ý tưởng chung là thực hiện logic càng sớm càng tốt - thất bại nhanh nếu bạn muốn. Vì vậy, các tình huống như sau:

  • cấu trúc của chính đối tượng lệnh; Hàm tạo của lệnh có một số trường bắt buộc phải có để lệnh được tạo; đây là xác nhận đầu tiên và nhanh nhất; Điều này rõ ràng được chứa trong lệnh.
  • xác thực trường cấp thấp, như tính không trống của một số trường (như tên người dùng) hoặc định dạng (địa chỉ email hợp lệ). Kiểu xác nhận này nên được chứa trong chính lệnh, trong hàm tạo. Có một phong cách khác là có một isValidphương thức nhưng điều này dường như vô nghĩa đối với tôi vì ai đó sẽ phải nhớ gọi phương thức này khi thực tế lệnh khởi tạo thành công là đủ.
  • riêng biệt command validators, các lớp có trách nhiệm xác nhận một lệnh. Tôi sử dụng loại xác nhận này khi tôi cần kiểm tra thông tin từ nhiều tổng hợp hoặc các nguồn bên ngoài. Bạn có thể sử dụng điều này để kiểm tra tính duy nhất của tên người dùng. Command validatorscó thể có bất kỳ phụ thuộc được tiêm, như kho lưu trữ. Hãy nhớ rằng xác thực này cuối cùng phù hợp với tổng hợp (nghĩa là khi người dùng được tạo, người dùng khác có cùng tên người dùng có thể được tạo trong thời gian này)! Ngoài ra, đừng cố gắng đưa vào đây logic nên nằm trong tổng hợp! Trình xác nhận lệnh khác với trình quản lý Sagas / Process tạo ra các lệnh dựa trên các sự kiện.
  • các phương thức tổng hợp nhận và xử lý các lệnh. Đây là xác nhận (loại) cuối cùng xảy ra. Tổng hợp trích xuất dữ liệu từ lệnh và sử dụng một số logic nghiệp vụ cốt lõi mà nó chấp nhận (nó thực hiện các thay đổi về trạng thái của nó) hoặc từ chối nó. Logic này được kiểm tra một cách nhất quán mạnh mẽ. Đây là tuyến phòng thủ cuối cùng. Trong ví dụ của bạn, quy tắc When a car uses Electric engine the only allowed transmission type is Automaticnên được kiểm tra ở đây.

Tôi cảm thấy như vậy bởi vì phương thức lưu của kho lưu trữ có khả năng chấp nhận một tổng hợp đảm bảo rằng một khi tổng hợp được thông qua, tất cả các bất biến đều được đáp ứng. Khi logic (ví dụ như không trống rỗng) chỉ xuất hiện trong chính xác thực lệnh, một lập trình viên khác hoàn toàn có thể bỏ qua xác thực này và gọi phương thức lưu trong UserRep repository với một đối tượng Người dùng có thể dẫn đến lỗi cơ sở dữ liệu nghiêm trọng, vì email có thể đã quá lâu

Sử dụng các kỹ thuật trên không ai có thể tạo các lệnh không hợp lệ hoặc bỏ qua logic bên trong các tập hợp. Trình xác nhận lệnh được tự động tải + được gọi bởi CommandDispatchervì vậy không ai có thể gửi lệnh trực tiếp đến tổng hợp. Người ta có thể gọi một phương thức trên tổng hợp thông qua một lệnh nhưng không thể duy trì các thay đổi nên sẽ vô nghĩa / vô hại khi làm như vậy.

Làm việc như một nhà phát triển PHP chịu trách nhiệm tạo ra các hệ thống RESTful, việc diễn giải CQRS của tôi làm lệch đi một chút so với cách tiếp cận xử lý lệnh async tiêu chuẩn, chẳng hạn như đôi khi trả về kết quả từ các lệnh do cần xử lý đồng bộ các lệnh.

Tôi cũng là một lập trình viên PHP và tôi không trả lại bất cứ thứ gì từ các trình xử lý lệnh của mình (các phương thức tổng hợp trong biểu mẫu handleSomeCommand). Tuy nhiên, tôi thường xuyên trả lại thông tin cho máy khách / trình duyệt trong HTTP response, ví dụ ID của gốc tổng hợp mới được tạo hoặc một cái gì đó từ mô hình đọc nhưng tôi không bao giờ trả lại (thực sự không bao giờ ) bất cứ thứ gì từ các phương thức lệnh tổng hợp của mình. Thực tế đơn giản là lệnh đã được chấp nhận (và được xử lý - chúng ta đang nói về xử lý PHP đồng bộ, phải không?!) Là đủ.

Chúng tôi trả lại một cái gì đó cho trình duyệt (và vẫn đang thực hiện CQRS theo sách) vì CQRS không phải là một kiến ​​trúc cấp cao .

Một ví dụ về cách trình xác nhận lệnh hoạt động:

Đường dẫn của lệnh thông qua trình xác nhận lệnh trên đường đến Tập hợp


Liên quan đến chiến lược xác nhận của bạn, điểm số hai nhảy vào tôi như một nơi có khả năng logic sẽ được nhân đôi thường xuyên. Chắc chắn người ta sẽ muốn Người dùng tổng hợp để xác thực một email không trống và có hình thức tốt không? Điều này trở nên rõ ràng khi chúng tôi giới thiệu lệnh ChangeEmail.
vua trượt bên

@ king-side-slide không nếu bạn có một EmailAddressđối tượng giá trị tự xác nhận hợp lệ.
Constantin Galbenu

Điều đó là hoàn toàn chính xác. Người ta có thể gói gọn một EmailAddressđể giảm trùng lặp. Quan trọng hơn, mặc dù vậy, bạn cũng sẽ chuyển logic ra khỏi lệnh và vào miền của mình. Điều đáng chú ý là điều này có thể được đưa quá xa. Thông thường các phần kiến ​​thức tương tự (đối tượng giá trị) có thể có các yêu cầu xác nhận khác nhau tùy thuộc vào người sử dụng chúng. EmailAddresslà một ví dụ thuận tiện vì toàn bộ quan niệm về giá trị này có các yêu cầu xác nhận toàn cầu.
vua trượt bên

Tương tự, ý tưởng về "trình xác nhận lệnh" dường như không cần thiết. Mục tiêu không phải là để ngăn chặn các lệnh không hợp lệ được tạo và gửi đi. Mục tiêu là để ngăn chặn chúng thực thi. Ví dụ: tôi có thể chuyển bất kỳ dữ liệu nào tôi muốn bằng một URL. Nếu nó không hợp lệ, hệ thống sẽ từ chối yêu cầu của tôi. Lệnh vẫn được tạo và gửi đi. Nếu một lệnh yêu cầu nhiều tổng hợp để xác thực (nghĩa là một tập hợp Người dùng để kiểm tra tính duy nhất của email), một dịch vụ miền là phù hợp hơn. Các đối tượng như "x xác thực" thường là một dấu hiệu của một mô hình thiếu máu trong đó dữ liệu đang được tách ra khỏi hành vi.
vua trượt bên

1
@ king-side-slide Một ví dụ cụ thể là UserCanPlaceOrdersOnlyIfHeIsNotLockedValidator. Bạn có thể thấy rằng đây là một miền riêng biệt của Đơn hàng để chính nó không thể được xác thực bởi chính OrderAggregate.
Constantin Galbenu

6

Một tiền đề cơ bản của DDD là các mô hình miền xác nhận chính chúng. Đây là một khái niệm quan trọng bởi vì nó nâng tên miền của bạn thành bên chịu trách nhiệm để đảm bảo các quy tắc kinh doanh của bạn được thực thi. Nó cũng giữ cho mô hình miền của bạn là trọng tâm để phát triển.

Một hệ thống CQRS (như bạn chỉ ra một cách chính xác) là một chi tiết triển khai đại diện cho một tên miền phụ chung thực hiện cơ chế gắn kết riêng của nó. Mô hình của bạn không nên phụ thuộc vào bất kỳ cơ sở hạ tầng CQRS nào để hành xử theo các quy tắc kinh doanh của bạn. Mục tiêu của DDD là mô hình hóa hành vi của một hệ thống sao cho kết quả là sự trừu tượng hóa hữu ích các yêu cầu chức năng của lĩnh vực kinh doanh cốt lõi của bạn. Tuy nhiên, việc chuyển bất kỳ phần nào của hành vi này ra khỏi mô hình của bạn, sẽ làm giảm tính toàn vẹn và sự gắn kết của mô hình của bạn (và làm cho nó ít hữu ích hơn).

Chỉ cần mở rộng ví dụ của bạn để bao gồm một ChangeEmaillệnh, chúng tôi có thể minh họa hoàn hảo lý do tại sao bạn không muốn bất kỳ logic kinh doanh nào trong cơ sở hạ tầng lệnh của mình vì bạn cần sao chép quy tắc của mình:

  • email không thể để trống
  • email không thể dài hơn 100 ký tự
  • email phải là duy nhất

Vì vậy, bây giờ chúng ta có thể chắc chắn rằng logic của chúng ta cần phải ở trong miền của chúng ta, hãy giải quyết vấn đề "ở đâu". Hai quy tắc đầu tiên có thể dễ dàng được áp dụng cho Usertổng hợp của chúng tôi , nhưng quy tắc cuối cùng đó có một chút sắc thái hơn; một trong đó đòi hỏi một số kiến ​​thức sâu hơn nữa để có được cái nhìn sâu sắc hơn. Nhìn bề ngoài, có vẻ như quy tắc này áp dụng cho a User, nhưng thực sự không phải vậy. "Tính duy nhất" của một email áp dụng cho một bộ sưu tập Users(theo một số phạm vi).

À ha! Với ý nghĩ đó, nó trở nên rõ ràng rõ ràng rằng UserRepository(bộ sưu tập trong bộ nhớ của bạn Users) có thể là một ứng cử viên tốt hơn để thực thi bất biến này. Phương thức "lưu" có thể là nơi hợp lý nhất để bao gồm séc (nơi bạn có thể ném UserEmailAlreadyExistsngoại lệ). Ngoài ra, một tên miền UserServicecó thể chịu trách nhiệm tạo mới Usersvà cập nhật các thuộc tính của chúng.

Thất bại nhanh là một cách tiếp cận tốt, nhưng chỉ có thể được thực hiện ở đâu và khi nó phù hợp với phần còn lại của mô hình. Việc kiểm tra các tham số trên một phương thức dịch vụ ứng dụng (hoặc lệnh) có thể cực kỳ hấp dẫn trước khi xử lý thêm để cố gắng bắt lỗi khi bạn (nhà phát triển) biết rằng cuộc gọi sẽ thất bại ở đâu đó trong quá trình. Nhưng khi làm như vậy, bạn sẽ có kiến ​​thức trùng lặp (và bị rò rỉ) theo cách có thể sẽ yêu cầu nhiều hơn một bản cập nhật mã khi quy tắc kinh doanh thay đổi.


2
Tôi đồng ý với điều này. Việc đọc của tôi cho đến bây giờ (không có CQRS) cho tôi biết rằng xác nhận phải luôn luôn đi trong mô hình miền để bảo vệ các bất biến. Bây giờ tôi đang đọc CQRS, nó bảo tôi đặt xác nhận vào các đối tượng Command. Điều này có vẻ phản trực giác. Bạn có biết bất kỳ ví dụ nào không, ví dụ như trên GitHub, nơi xác thực được đặt trong Mô hình miền thay vì Lệnh? +1.
w0051977
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.