Nơi nào bạn nên xác nhận trạng thái của các tập hợp khác của thành phố khác?


8

Kịch bản:

Một khách hàng đặt hàng, sau đó, sau khi nhận được sản phẩm, cung cấp phản hồi về quy trình đặt hàng.

Giả sử các gốc tổng hợp sau:

  • khách hàng
  • Đặt hàng
  • Phản hồi

Dưới đây là các quy tắc kinh doanh:

  1. Một khách hàng chỉ có thể cung cấp phản hồi về đơn đặt hàng của họ, không phải của người khác.
  2. Một khách hàng chỉ có thể cung cấp thông tin phản hồi nếu đơn đặt hàng đã được thanh toán.

    class Feedback {
        public function __construct($feedbackId,
                                    Customer $customer,
                                    Order $order,
                                    $content) {
            if ($customer->customerId() != $order->customerId()) {
                // Error
            }
            if (!$order->isPaid()) {
                // Error
            }
            $this->feedbackId = $feedbackId;
            $this->customerId = $customerId;
            $this->orderId = $orderId;
            $this->content = $content;
        }
    }
    

Bây giờ, giả sử doanh nghiệp muốn một quy tắc mới:

  1. Một khách hàng chỉ có thể cung cấp thông tin phản hồi nếu Supplierhàng hóa của đơn đặt hàng vẫn đang hoạt động.

    class Feedback {
        public function __construct($feedbackId,
                                    Customer $customer,
                                    Order $order,
                                    Supplier $supplier,
                                    $content) {
            if ($customer->customerId() != $order->customerId()) {
                // Error
            }
            if (!$order->isPaid()) {
                // Error
            }
            // NEW RULE HERE
            if (!$supplier->isOperating()) {
                // Error
            }
            $this->feedbackId = $feedbackId;
            $this->customerId = $customerId;
            $this->orderId = $orderId;
            $this->content = $content;
        }
    }
    

Tôi đã đặt việc thực hiện hai quy tắc đầu tiên trong Feedback chính tổng hợp. Tôi cảm thấy thoải mái khi làm điều này, đặc biệt là khi Feedbacktổng hợp tham chiếu tất cả các tổng hợp khác theo danh tính. Ví dụ, các thuộc tính của Feedbackthành phần chỉ ra rằng nó biết về sự tồn tại của các tập hợp khác, vì vậy tôi cảm thấy thoải mái khi biết về trạng thái chỉ đọc của các tập hợp này.

Tuy nhiên, dựa trên các thuộc tính của nó, Feedbacktập hợp không có kiến ​​thức về sự tồn tại của Suppliertập hợp, vậy nó có nên có kiến ​​thức về trạng thái chỉ đọc của tập hợp này không?

Giải pháp thay thế để thực hiện quy tắc 3 là di chuyển logic này đến mức phù hợp CommandHandler. Tuy nhiên, điều này cảm thấy như nó di chuyển logic miền ra khỏi "trung tâm" của kiến ​​trúc dựa trên hành tây của tôi.

Tổng quan về kiến ​​trúc củ hành của tôi


Giao diện kho lưu trữ là một phần của miền. Vì vậy, logic xây dựng (mà bản thân nó được coi là một dịch vụ trong sách DDD) có thể gọi kho lưu trữ của Đơn hàng để hỏi xem nhà cung cấp của Đơn hàng có còn hoạt động không.
Euphoric

Thứ nhất, Suppliertrạng thái hoạt động của tổng hợp sẽ không được truy vấn thông qua Orderkho lưu trữ; SupplierOrderlà hai tập hợp riêng biệt. Thứ hai, có một câu hỏi trong danh sách gửi thư DDD / CQRS về việc chuyển các gốc và kho tổng hợp cho các phương thức gốc tổng hợp khác (bao gồm cả hàm tạo). Có nhiều ý kiến ​​khác nhau, nhưng Greg Young đã đề cập rằng việc chuyển các gốc tổng hợp làm tham số là phổ biến, trong khi một người khác nói rằng các kho lưu trữ có liên quan chặt chẽ hơn đến cơ sở hạ tầng hơn là miền. Ví dụ, kho "trừu tượng trong bộ sưu tập bộ nhớ" và không có logic.
mộc lan

Nhà cung cấp không liên quan đến Đặt hàng? Điều gì xảy ra khi Nhà cung cấp không liên quan đến Đơn hàng được thông qua? Vâng, "là nhà cung cấp hoạt động" không phải là một logic. Đó là truy vấn đơn giản. Ngoài ra, có lý do tại sao nó phổ biến: Không có nó, mã của bạn trở nên phức tạp hơn nhiều và yêu cầu chuyển thông tin xung quanh nơi có thể xảy ra lỗi. Ngoài ra, "giao diện kho lưu trữ" không phải là cơ sở hạ tầng. Việc thực hiện kho lưu trữ là.
Euphoric

Bạn đúng. Giống như Customerchỉ có thể cung cấp phản hồi về một trong các đơn đặt hàng của riêng họ ( $order->customerId() == $customer->customerId()), chúng tôi cũng phải so sánh ID nhà cung cấp ( $order->supplierId() == $supplier->supplierId()). Các quy tắc bảo vệ đầu tiên chống lại người dùng cung cấp các giá trị không chính xác. Các quy tắc bảo vệ thứ hai chống lại các lập trình viên cung cấp các giá trị không chính xác. Tuy nhiên, việc kiểm tra xem nhà cung cấp đang hoạt động phải ở trong Feedbackthực thể hay trong bộ xử lý lệnh. Câu hỏi ở đâu
mộc lan

2
Hai ý kiến, không liên quan trực tiếp đến câu hỏi. Đầu tiên, việc chuyển các gốc Tổng hợp làm đối số cho một tổng hợp khác có vẻ sai - chúng phải là Id - không có gì hữu ích mà tổng hợp có thể làm với tổng hợp khác. Thứ hai, Khách hàng và Nhà cung cấp rất ... khó khăn, sổ ghi chép trong cả hai trường hợp là thế giới thực: bạn không thể ngăn nhà cung cấp trong thế giới thực bằng cách gửi lệnh CeaseOperations đến mô hình miền của bạn.
VoiceOfUnreason

Câu trả lời:


1

Nếu tính chính xác trong giao dịch yêu cầu một tổng hợp biết về trạng thái hiện tại của một tổng hợp khác, thì mô hình của bạn đã sai.

Trong hầu hết các trường hợp, tính chính xác trong giao dịch là không bắt buộc . Các doanh nghiệp có xu hướng có dung sai xung quanh dữ liệu độ trễ và cũ. Điều này đặc biệt đúng với sự không nhất quán dễ phát hiện và dễ khắc phục.

Vì vậy, lệnh sẽ được chạy bởi tổng hợp thay đổi trạng thái. Để thực hiện kiểm tra không nhất thiết đúng, nó cần một bản sao mới nhất của trạng thái của tổng hợp khác.

Đối với các lệnh trên tổng hợp hiện có, mẫu thông thường là chuyển Kho lưu trữ đến tổng hợp và tổng hợp sẽ chuyển trạng thái của nó tới kho lưu trữ, cung cấp truy vấn trả về trạng thái / phép chiếu bất biến của tổng hợp khác

class Feedback {
    void downvote(Repository<Supplier.State> query) {
        Supplier.State supplier = query.getById(this->supplierId);
        boolean isOperating = state.isOperating();
        ....
    }
}

Nhưng các mẫu xây dựng rất kỳ lạ - khi bạn đang tạo đối tượng, người gọi đã biết trạng thái bên trong, bởi vì nó đang cung cấp nó. Mô hình tương tự hoạt động, nó chỉ trông vô nghĩa

class Feedback {
    __construct(SupplierId supplierId, SupplierOperatingQuery query ...) {
        Supplier.State supplier = query.getById(this->supplierId);
        boolean isOperating = state.isOperating();
        ....
    }
}

Chúng tôi tuân theo các quy tắc bằng cách giữ tất cả logic miền trong các đối tượng miền, nhưng chúng tôi không thực sự bảo vệ bất biến doanh nghiệp theo bất kỳ cách hữu ích nào bằng cách làm như vậy (vì tất cả các thông tin tương tự đều có sẵn cho thành phần ứng dụng). Đối với mô hình sáng tạo, nó sẽ tốt như viết

class Feedback {
    __construct(Supplier.State supplier, ...) {
        boolean isOperating = state.isOperating();
        ....
    }
}

1. SupplierOperatingQueryTruy vấn mô hình đọc hoặc "Truy vấn" trong tên có gây hiểu nhầm không? 2. Tính nhất quán trong giao dịch là không bắt buộc. Sẽ không có vấn đề gì nếu nhà cung cấp ngừng hoạt động một giây trước khi khách hàng để lại phản hồi, nhưng điều đó có nghĩa là chúng ta không nên kiểm tra nó? 3. Trong ví dụ của bạn, việc cung cấp "dịch vụ truy vấn" chứ không phải chính đối tượng thực thi tính nhất quán giao dịch? Nếu vậy thì thế nào? 4. Việc sử dụng các dịch vụ truy vấn đó ảnh hưởng đến thử nghiệm đơn vị như thế nào?
Magnus

1. Truy vấn, theo nghĩa là gọi nó không thay đổi trạng thái của bất cứ điều gì. 3. Không có sự thống nhất về giao dịch với dịch vụ truy vấn, không có sự tương tác giữa nó và lệnh chạy đồng thời đang sửa đổi tổng hợp khác. 4. Trong trường hợp này, nó sẽ là một phần của mô hình miền, vì vậy chỉ cần cung cấp một triển khai thử nghiệm. Hmm, tuy nhiên điều đó hơi kỳ lạ - DomainService có thể không phải là thuật ngữ tốt nhất để sử dụng.
VoiceOfUnreason

2. Hãy ghi nhớ, vì dữ liệu bạn đang sử dụng ở đây nằm trong ranh giới tổng hợp, séc của bạn có thể cho bạn câu trả lời sai (ví dụ: séc của bạn cho biết không ổn, nhưng đó là do tổng hợp khác đang thay đổi). Vì vậy, tốt hơn là nên chuyển kiểm tra đó sang mô hình đọc (luôn chấp nhận lệnh, nhưng tạo một báo cáo ngoại lệ nếu mô hình không nhất quán). Bạn cũng có thể sắp xếp rằng máy khách chỉ gửi các lệnh được cho là thành công - tức là máy khách không nên gửi các lệnh mà nó dự kiến ​​sẽ thất bại, dựa trên sự hiểu biết về trạng thái hiện tại của nó.
VoiceOfUnreason

1. Nó thường cau mày vì "bên viết" để truy vấn "bên đọc" (ví dụ: các phép chiếu có nguồn gốc sự kiện). "... theo nghĩa là gọi nó không thay đổi trạng thái của bất cứ điều gì" - đơn giản là không sử dụng một trình truy cập bất biến, mà tôi sẽ tranh luận là đơn giản hơn nhiều. 2. Sẽ tốt hơn nếu nhân đôi kiểm tra trong mô hình đọc, nhưng nếu bạn di chuyển nó (đọc: Di chuyển nó từ máy chủ), bạn sẽ tự tạo ra vấn đề. Thứ nhất, quy tắc kinh doanh của bạn phải được nhân đôi trong mỗi máy khách (trình duyệt web và máy khách di động). Thứ hai, thật đơn giản để bỏ qua kiểm tra này:
Magnus

3. "... không có tương tác giữa nó và lệnh đang chạy đồng thời đang sửa đổi tổng hợp khác" - cũng không tải tổng hợp Nhà cung cấp, vì chỉ có tổng hợp Phản hồi đang được sửa đổi. 4. Vì vậy, CarrierOperatingQuery là một giao diện yêu cầu triển khai cụ thể, nghĩa là bạn phải tạo một triển khai giả trong thử nghiệm đơn vị của mình chỉ để kiểm tra giá trị đúng / sai của một biến đã tồn tại trong đối tượng khác? Mùi như quá mức cần thiết. Tại sao không tạo một CustomerOwnsOrderQuery và OrderIsPaidQuery?
Magnus

-1

Tôi biết đây là một câu hỏi cũ, nhưng tôi muốn chỉ ra rằng vấn đề trực tiếp bắt nguồn từ một tiền đề không chính xác. Đó là, các gốc tổng hợp mà chúng tôi muốn giả sử tồn tại đơn giản là không chính xác.

Chỉ có một gốc tổng hợp trong hệ thống mà bạn đã mô tả: Khách hàng. Cả một Đơn đặt hàng và Phản hồi, trong khi chúng có thể được tổng hợp theo cách riêng của chúng, đều phụ thuộc vào Khách hàng để tồn tại, vì vậy bản thân chúng không phải là gốc tổng hợp. Logic bạn cung cấp trong hàm tạo phản hồi của bạn dường như chỉ ra rằng một Đơn hàng PHẢI có khách hàng và Phản hồi PHẢI cũng liên quan đến Khách hàng. Điều này thật ý nghĩa. Làm thế nào để một Đơn đặt hàng hoặc Phản hồi không liên quan đến Khách hàng? Ngoài ra, Nhà cung cấp dường như có liên quan một cách hợp lý đến Đơn hàng (vì vậy sẽ nằm trong tổng hợp này).

Với ý tưởng trên, tất cả thông tin bạn muốn đã có sẵn trong thư mục gốc tổng hợp của Khách hàng và rõ ràng là bạn đang thực thi các quy tắc của mình ở sai vị trí. Các nhà xây dựng là nơi khủng khiếp để thực thi các quy tắc kinh doanh và nên tránh bằng mọi giá. Đây là giao diện của nó (Lưu ý: Tôi sẽ không bao gồm các nhà xây dựng cho Khách hàng và Đặt hàng vì các nhà máy có thể được sử dụng. Cũng không hiển thị tất cả các phương thức giao diện).

/*******************************\
   Interfaces, explained below 
\*******************************/

interface ICustomer
{
    public function getId() : int;
}

interface IUser extends ICustomer
{
    public function getUsername() : string;

    public function getPassword() : string;

    public function changeUsername( string $new ) : void;

    public function resetPassword( string $new ) : void;

}

interface IReviewer extends ICustomer
{
    public function provideFeedback( IOrder $order, string $content ) : void;
}

interface IBuyer extends ICustomer
{
    public function placeOrder( IOrder $order ) : void;
}

interface IOrder
{
    public function getCustomerId() : int;

    public function addFeedback( string $content ) : void;
}


interface IFeedback
{
    public function addContent( string $content ) : void;

    public function isValidContent( string $content ) : void;
}



/*******************************\
   Implentation
\*******************************/



class Customer implements IReviewer, IBuyer
{
    protected $id;

    protected $orders = [];

    public function provideFeedback( IOrder $order, string $content ) : void
    {
        if( $order->getCustomerId() !== $this->getId() )
            throw new \InvalidArgumentException('Customers can only provide feedback on their own orders');

        $order->addFeedback( $content );
    }
}


class Order implements IOrder
{
    protected $supplier;

    protected $feedbacks = [];

    public function addFeedback( string $content ) : void
    {
        if( false === $this->supplier->isOperating() )
            throw new \Exception('Feedback can only be added to orders if the supplier is still operating.');

        // could be any IFeedback
        $feedback = new Feedback( $this );

        $feedback->addContent( $content );

        $this->feedbacks[] = $feedback;
    }
}


class Feedback implements IFeedback
{
    protected $order;

    protected $content;

    public function __construct( IOrder $order )
    {    
         // we don't carry our business rules in constructors
         $this->order = $order;
    }

    public function addContent( string $content ) : void
    {
        if( false === $this->isValidContent($content) )
            throw new \Exception("Content contains offensive language.");

        $this->content = $content;
    }
}

Được chứ. Hãy phá vỡ điều này một chút. Điều đầu tiên bạn sẽ nhận thấy là mô hình này khai báo nhiều hơn bao nhiêu. Tất cả mọi thứ là một hành động, nó trở nên rõ ràng quy tắc kinh doanh WHERE nên áp dụng. Thiết kế ở trên không chỉ "làm" đúng, nó "nói" đúng.

Điều gì sẽ khiến bất cứ ai cho rằng các quy tắc đang được thực thi trong dòng sau đây?

// this is a BAD place for rules to execute
$feedback = new Feedback( $id, $customerId, $order, $supplier, $content);

Thứ hai, bạn có thể thấy rằng tất cả các logic liên quan đến việc xác thực các quy tắc kinh doanh được thực hiện càng sát càng tốt với các mô hình mà chúng liên quan. Trong ví dụ của bạn, hàm tạo (một phương thức duy nhất) đang thực hiện nhiều xác nhận hợp lệ đối với các mô hình khác nhau. Điều đó phá vỡ thiết kế RẮN. Chúng tôi sẽ thêm một kiểm tra ở đâu để đảm bảo rằng nội dung Phản hồi không chứa từ xấu? Một kiểm tra khác trong các nhà xây dựng? Điều gì xảy ra nếu các loại Phản hồi khác nhau cần kiểm tra nội dung khác nhau? Xấu xí.

Thứ ba, nhìn vào các giao diện, bạn có thể thấy có những nơi tự nhiên để mở rộng / sửa đổi các quy tắc thông qua thành phần. Ví dụ, các loại đơn đặt hàng khác nhau có thể có các quy tắc khác nhau về thời điểm phản hồi có thể được cung cấp. Đặt hàng cũng có thể cung cấp các loại phản hồi khác nhau, do đó có thể có các quy tắc khác nhau để xác nhận.

Bạn cũng có thể thấy một loạt các giao diện ICustomer *. Chúng được sử dụng để soạn tổng hợp Khách hàng mà chúng tôi cần ở đây (có thể không chỉ gọi là Khách hàng). Lý do cho điều này là đơn giản. RẤT có khả năng Khách hàng là một gốc tổng hợp LỚN lan rộng ra khắp miền / DB của bạn. Bằng cách sử dụng các giao diện, chúng ta có thể phân tách một tổng hợp (có khả năng quá lớn để tải) thành nhiều gốc tổng hợp chỉ cung cấp các hành động nhất định (như đặt hàng hoặc cung cấp phản hồi). Bạn có thể thấy tổng hợp trong triển khai của tôi có thể CẢ HAI đơn đặt hàng VÀ cung cấp phản hồi, nhưng không thể được sử dụng để đặt lại mật khẩu hoặc thay đổi tên người dùng.

Vì vậy, câu trả lời cho câu hỏi của bạn là tập hợp nên tự xác nhận. Nếu họ không thể có một mô hình thiếu.


1
Mặc dù các ranh giới tổng hợp là khác nhau tùy thuộc vào người thiết kế hệ thống, tôi nghĩ rằng một tập hợp cốt lõi xuất phát từ thứ tự chỉ là ngớ ngẩn. Ví dụ của bạn về Nhà cung cấp là một phần của đơn đặt hàng là một trường hợp tốt - Nhà cung cấp có thể không tồn tại cho đến khi Đơn hàng được tạo không? Điều gì về các nhà cung cấp trùng lặp:
Magnus

@ user1420752 Tôi nghĩ bạn có thể có nó ngược. Mô hình trên ngụ ý ngược lại. Rằng một Đơn hàng không thể tồn tại mà không có Nhà cung cấp. Ví dụ của tôi chỉ đơn giản là sử dụng thông tin / quy tắc / mối quan hệ mà tôi có thể lượm lặt được từ mã được cung cấp. Tôi đồng ý rằng, giống như Khách hàng, Đơn hàng có thể là một tập hợp lớn, phức tạp theo đúng nghĩa của nó (mặc dù không phải là một gốc). Một trong đó cũng có thể yêu cầu phân tách thành một số ít các triển khai cụ thể tùy thuộc vào bối cảnh. Điểm tôi đang minh họa là các thực thể PHẢI tự xác nhận. Như bạn có thể thấy, nó sạch hơn theo cách đó.
king-side-slide

@ user1420752 Tôi muốn thêm rằng thường các phương thức / hàm tạo yêu cầu nhiều đối số là dấu hiệu của một mô hình thiếu máu trong đó dữ liệu được tách ra khỏi hành vi (và do đó cần phải được đưa vào các khối lớn vào các phần hoạt động trên dữ liệu ). Hàm tạo phản hồi bạn cung cấp là một ví dụ về điều này. Các mô hình thiếu máu có xu hướng giảm sự gắn kết và thêm ngữ nghĩa khớp nối thêm (như kiểm tra ID nhiều lần). Sự gắn kết cao thường có nghĩa là mọi phương thức trong một thực thể đều sử dụng tất cả các biến thể hiện của nó. Điều này tự nhiên dẫn đến sự phân hủy của các tập hợp lớn như Khách hàng hoặc Đơn hàng
king-side-slide
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.