Dịch vụ tiêm chích DDD trên các phương thức thực thể


11

Định dạng ngắn của câu hỏi

Có phải trong các thực tiễn tốt nhất của DDD và OOP để tiêm dịch vụ vào các cuộc gọi phương thức thực thể không?

Ví dụ định dạng dài

Giả sử chúng ta có trường hợp Order-LineItems cổ điển trong DDD, trong đó chúng ta có một Thực thể Miền được gọi là Đơn hàng, cũng hoạt động như Root tổng hợp, và Thực thể đó không chỉ bao gồm các Đối tượng Giá trị, mà còn là một tập hợp các Mục hàng Các thực thể.

Giả sử chúng ta muốn cú pháp trôi chảy trong ứng dụng của mình, để chúng ta có thể làm một cái gì đó như thế này (lưu ý cú pháp trong dòng 2, nơi chúng ta gọi getLineItemsphương thức):

$order = $orderService->getOrderByID($orderID);
foreach($order->getLineItems($orderService) as $lineItem) {
  ...
}

Chúng tôi không muốn đưa bất kỳ loại LineItemRep repository nào vào OrderEntity, vì đó là vi phạm một số nguyên tắc mà tôi có thể nghĩ ra. Nhưng, sự trôi chảy của cú pháp là điều mà chúng tôi thực sự muốn, bởi vì nó dễ đọc và duy trì, cũng như kiểm tra.

Hãy xem xét các đoạn mã sau, lưu ý phương thức getLineItemstrong OrderEntity:

interface IOrderService {
    public function getOrderByID($orderID) : OrderEntity;
    public function getLineItems(OrderEntity $orderEntity) : LineItemCollection;
}

class OrderService implements IOrderService {
    private $orderRepository;
    private $lineItemRepository;

    public function __construct(IOrderRepository $orderRepository, ILineItemRepository $lineItemRepository) {
        $this->orderRepository = $orderRepository;
        $this->lineItemRepository = $lineItemRepository;
    }

    public function getOrderByID($orderID) : OrderEntity {
        return $this->orderRepository->getByID($orderID);
    }

    public function getLineItems(OrderEntity $orderEntity) : LineItemCollection {
        return $this->lineItemRepository->getLineItemsByOrderID($orderEntity->ID());
    }
}

class OrderEntity {
    private $ID;
    private $lineItems;

    public function getLineItems(IOrderServiceInternal $orderService) {
        if(!is_null($this->lineItems)) {
            $this->lineItems = $orderService->getLineItems($this);
        }
        return $this->lineItems;
    }
}

Đó có phải là cách được chấp nhận để thực hiện cú pháp trôi chảy trong Thực thể mà không vi phạm các nguyên tắc cốt lõi của DDD và OOP? Đối với tôi có vẻ ổn, vì chúng tôi chỉ phơi bày lớp dịch vụ, không phải lớp cơ sở hạ tầng (được lồng trong dịch vụ)

Câu trả lời:


9

Hoàn toàn ổn khi vượt qua một dịch vụ Miền trong một cuộc gọi thực thể. Giả sử, chúng ta cần tính tổng hóa đơn với một số thuật toán phức tạp có thể phụ thuộc vào loại khách hàng. Đây là những gì nó có thể trông giống như:

class Invoice
{
    private $currency;
    private $customerId;

    public function __construct()
    {
    }

    public function sum(InvoiceCalculator $calculator)
    {
        $sum =
            new SumRecord(
                $calculator->calculate($this)
            )
        ;

        if ($sum->isZero()) {
            $this->events->add(new ZeroSumCalculated());
        }

        return $sum;
    }
}

Một cách tiếp cận khác là tách ra một logic nghiệp vụ được đặt trong dịch vụ miền thông qua các sự kiện miền . Hãy nhớ rằng phương pháp này chỉ ngụ ý các dịch vụ ứng dụng khác nhau, nhưng cùng phạm vi giao dịch cơ sở dữ liệu.

Cách tiếp cận thứ ba là cách tôi ủng hộ: nếu tôi thấy mình đang sử dụng dịch vụ tên miền, điều đó có nghĩa là tôi đã bỏ lỡ một số khái niệm miền, vì tôi mô hình hóa các khái niệm của mình chủ yếu bằng danh từ , không phải động từ. Vì vậy, lý tưởng nhất là tôi không cần một dịch vụ tên miền nào cả và một phần tốt trong tất cả logic kinh doanh của tôi nằm trong các nhà trang trí .


6

Tôi bị sốc khi đọc một số câu trả lời ở đây.

Hoàn toàn hợp lệ khi chuyển các dịch vụ miền vào các phương thức thực thể trong DDD để ủy thác một số tính toán kinh doanh. Ví dụ, hãy tưởng tượng gốc tổng hợp của bạn (một thực thể) cần truy cập vào một tài nguyên bên ngoài thông qua http để thực hiện một số logic nghiệp vụ và đưa ra một sự kiện. Nếu bạn không tiêm dịch vụ thông qua phương thức kinh doanh của thực thể, bạn sẽ làm thế nào khác? Bạn có thể khởi tạo một máy khách http bên trong thực thể của mình không? Nghe có vẻ là một ý tưởng khủng khiếp.

Điều không chính xác là tiêm các dịch vụ trong tập hợp thông qua hàm tạo của nó. Nhưng thông qua một phương pháp kinh doanh, nó ổn và hoàn toàn bình thường.


1
Tại sao trường hợp bạn đưa ra không phải là trách nhiệm của Dịch vụ Miền?
e_i_pi

1
đó là một dịch vụ tên miền, nhưng nó được đưa vào phương thức kinh doanh. Lớp ứng dụng chỉ là một dàn nhạc,
diegosasw

Tôi chưa có kinh nghiệm về DDD nhưng không nên gọi Dịch vụ miền từ Dịch vụ ứng dụng và sau khi xác thực Dịch vụ miền tiếp tục gọi các phương thức Thực thể thông qua Dịch vụ ứng dụng đó? Tôi đang đối mặt với cùng một vấn đề trong dự án của mình, vì Dịch vụ miền chạy cuộc gọi cơ sở dữ liệu qua kho lưu trữ ... Tôi không biết liệu điều này có ổn không.
Muflix

Dịch vụ tên miền nên phối hợp, nếu bạn gọi nó từ ứng dụng sau, điều đó có nghĩa là bạn bằng cách nào đó xử lý phản hồi và sau đó bạn làm gì đó với nó. Có lẽ điều đó nghe giống như logic kinh doanh. Nếu vậy, nó thuộc về lớp Miền và ứng dụng sau đó chỉ đơn giản là giải quyết sự phụ thuộc và đưa nó vào tổng hợp. Dịch vụ miền có thể đã chèn một kho lưu trữ có cơ sở dữ liệu đánh vào việc thực hiện nên thuộc về lớp cơ sở hạ tầng (chỉ là việc thực hiện chứ không phải giao diện / hợp đồng). Nếu nó mô tả ngôn ngữ phổ biến của bạn, nó thuộc về miền.
diegosasw

5

Có phải trong các thực tiễn tốt nhất của DDD và OOP để tiêm dịch vụ vào các cuộc gọi phương thức thực thể không?

Không, bạn không nên tiêm bất cứ thứ gì vào bên trong lớp miền của mình (điều này bao gồm các thực thể, đối tượng giá trị, nhà máy và dịch vụ miền). Lớp này không thể biết về bất kỳ khuôn khổ, thư viện hoặc công nghệ của bên thứ 3 nào và không nên thực hiện bất kỳ cuộc gọi IO nào.

$order->getLineItems($orderService)

Điều này là sai vì Uẩn không cần bất cứ thứ gì khác ngoài chính nó để trả về các mục đặt hàng. Các toàn bộ tổng hợp nên đã được nạp trước khi gọi phương thức của nó. Nếu bạn cảm thấy rằng điều này nên được tải lười biếng thì có hai khả năng:

  1. Ranh giới Tổng hợp của bạn là sai, chúng quá lớn.

  2. Trong usecase này, bạn chỉ sử dụng Tổng hợp để đọc. Giải pháp tốt nhất là tách mô hình ghi từ mô hình đọc (tức là sử dụng CQRS ). Trong kiến trúc sạch hơn này, bạn không được phép truy vấn Tổng hợp mà là mô hình đọc.


Nếu tôi cần gọi cơ sở dữ liệu để xác thực, tôi phải gọi nó trong dịch vụ ứng dụng và chuyển một kết quả cho dịch vụ miền hoặc trực tiếp vào tổng hợp gốc thay vì tiêm kho lưu trữ vào dịch vụ miền?
Muflix

1
@Muflix vâng, đúng vậy
Constantin Galbenu

3

Ý tưởng chính trong các mẫu chiến thuật DDD: ứng dụng truy cập tất cả dữ liệu trong ứng dụng bằng cách tác động vào một gốc tổng hợp. Điều này ngụ ý rằng các thực thể duy nhất có thể truy cập bên ngoài mô hình miền là các gốc tổng hợp.

Rễ tổng hợp đơn hàng sẽ không bao giờ mang lại một tham chiếu đến bộ sưu tập lineitem cho phép bạn sửa đổi bộ sưu tập, cũng như không mang lại một bộ sưu tập các tham chiếu cho bất kỳ chi tiết đơn hàng nào cho phép bạn sửa đổi nó. Nếu bạn muốn thay đổi tổng hợp Đơn hàng, nguyên tắc hollywood áp dụng: "Nói, đừng hỏi".

Trả về các giá trị từ bên trong tổng hợp là tốt, bởi vì các giá trị vốn đã bất biến; bạn không thể thay đổi dữ liệu của tôi bằng cách thay đổi bản sao của nó.

Sử dụng một dịch vụ miền làm đối số, để hỗ trợ tổng hợp trong việc cung cấp các giá trị chính xác, là một điều hoàn toàn hợp lý để làm.

Thông thường, bạn sẽ không sử dụng dịch vụ tên miền để cung cấp quyền truy cập vào dữ liệu bên trong tổng hợp, vì tổng hợp đã có quyền truy cập vào dịch vụ đó.

$order = $orderService->getOrderByID($orderID);
foreach($order->getLineItems($orderService) as $lineItem) {
  ...
}

Vì vậy, chính tả đó là lạ, nếu chúng tôi đang cố gắng truy cập bộ sưu tập các giá trị chi tiết đơn hàng này. Chính tả tự nhiên hơn sẽ là

$order = $orderService->getOrderByID($orderID);
foreach($order->getLineItems() as $lineItem) {
  ...
}

Tất nhiên, điều này giả sử rằng các chi tiết đơn hàng đã được tải.

Mẫu thông thường là tải của tổng hợp sẽ bao gồm tất cả trạng thái cần thiết cho trường hợp sử dụng cụ thể. Nói cách khác, bạn có thể có một số cách khác nhau để tải cùng một tổng hợp; phương pháp lưu trữ của bạn là phù hợp cho mục đích .

Cách tiếp cận này không phải là thứ mà bạn sẽ tìm thấy trong Evans ban đầu, nơi ông cho rằng một tập hợp sẽ có một mô hình dữ liệu duy nhất được liên kết với nó. Nó rơi tự nhiên hơn ra khỏi CQRS.


Cảm ơn vì điều đó. Bây giờ tôi đã đọc khoảng một nửa "cuốn sách đỏ" và có sở thích đầu tiên là áp dụng đúng Nguyên tắc Hollywood trong lớp cơ sở hạ tầng. Đọc lại tất cả các câu trả lời này, tất cả chúng đều đưa ra những điểm tốt, nhưng tôi nghĩ rằng câu hỏi của bạn có một số điểm rất quan trọng liên quan đến phạm vi lineItems()và tải trước khi lấy lại Tổng hợp gốc.
e_i_pi

3

Nói chung, các đối tượng giá trị thuộc tổng hợp không có kho lưu trữ. Đó là trách nhiệm tổng hợp của rễ để cư trú chúng. Trong trường hợp của bạn, đó là trách nhiệm của OrderRep repository đối với cả các đối tượng giá trị thực thể Order và OrderLine.

Về việc triển khai cơ sở hạ tầng của OrderRep repository, trong trường hợp ORM, đó là mối quan hệ một-nhiều và bạn có thể chọn một cách háo hức hoặc lười biếng tải OrderLine.

Tôi không chắc dịch vụ của bạn chính xác là gì. Nó khá gần với "Dịch vụ ứng dụng". Nếu đây là trường hợp, nói chung, không nên đưa các dịch vụ vào Đối tượng gốc / Đối tượng / Giá trị tổng hợp. Dịch vụ ứng dụng phải là ứng dụng khách của Đối tượng gốc / Đối tượng / Giá trị và Dịch vụ miền. Một điều khác về dịch vụ của bạn là, phơi bày các đối tượng giá trị trong Dịch vụ ứng dụng cũng không phải là một ý tưởng hay. Chúng nên được truy cập bởi tổng hợp gốc.


2

Câu trả lời là: chắc chắn KHÔNG, tránh truyền dịch vụ trong các phương thức thực thể.

Giải pháp rất đơn giản: chỉ cần để kho lưu trữ Đơn hàng trả về Đơn hàng với tất cả LineItems của nó. Trong trường hợp của bạn, tổng hợp là Order + LineItems, vì vậy nếu kho lưu trữ không trả về một tổng hợp hoàn chỉnh, thì nó không hoạt động.

Nguyên tắc rộng hơn là: giữ các bit chức năng (ví dụ logic miền) tách biệt với các bit không chức năng (ví dụ: tính bền vững).

Một điều nữa: nếu bạn có thể, hãy cố gắng tránh làm điều này:

$order = $orderService->getOrderByID($orderID);
foreach($order->getLineItems() as $lineItem) {
  ...
}

Làm điều này thay thế

$order = $orderService->getOrderByID($orderID);
$order->doSomethingSignificant();

Trong thiết kế hướng đối tượng, chúng tôi cố gắng tránh câu cá xung quanh trong dữ liệu đối tượng. Chúng tôi muốn yêu cầu đối tượng làm những gì chúng tôi muốn.

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.