Có một cách thanh lịch để kiểm tra các mâu thuẫn độc đáo trên các thuộc tính đối tượng miền mà không chuyển logic nghiệp vụ vào lớp dịch vụ không?


10

Tôi đã thích nghi với thiết kế hướng tên miền trong khoảng 8 năm nay và thậm chí sau ngần ấy năm, vẫn còn một điều, điều đó đã làm tôi khó chịu. Đó là kiểm tra một bản ghi duy nhất trong lưu trữ dữ liệu đối với một đối tượng miền.

Vào tháng 9 năm 2013, Martin Fowler đã đề cập đến nguyên tắc TellD Don'tAsk , nếu có thể, nên được áp dụng cho tất cả các đối tượng miền, sau đó sẽ trả về một thông điệp, cách hoạt động (trong thiết kế hướng đối tượng, điều này chủ yếu được thực hiện thông qua các ngoại lệ, khi hoạt động không thành công).

Các dự án của tôi thường được chia thành nhiều phần, trong đó hai trong số đó là Miền (chứa các quy tắc kinh doanh và không có gì khác, tên miền hoàn toàn không biết gì) và Dịch vụ. Dịch vụ biết về lớp kho lưu trữ được sử dụng cho dữ liệu CRUD.

Bởi vì tính duy nhất của một thuộc tính thuộc về một đối tượng là một quy tắc miền / doanh nghiệp, nên nó phải dài đối với mô-đun miền, do đó quy tắc này chính xác là nơi nó được cho là.

Để có thể kiểm tra tính duy nhất của một bản ghi, bạn cần truy vấn bộ dữ liệu hiện tại, thường là cơ sở dữ liệu, để tìm hiểu xem liệu một bản ghi khác có giả sử Nameđã tồn tại hay chưa.

Xem xét lớp miền là không biết gì về sự kiên trì và không biết làm thế nào để lấy lại dữ liệu mà chỉ có cách thực hiện các thao tác trên chúng, nó thực sự không thể chạm vào các kho lưu trữ.

Thiết kế tôi đã được điều chỉnh sau đó trông như thế này:

class ProductRepository
{
    // throws Repository.RecordNotFoundException
    public Product GetBySKU(string sku);
}

class ProductCrudService
{
    private ProductRepository pr;

    public ProductCrudService(ProductRepository repository)
    {
        pr = repository;
    }

    public void SaveProduct(Domain.Product product)
    {
        try {
            pr.GetBySKU(product.SKU);

            throw Service.ProductWithSKUAlreadyExistsException("msg");
        } catch (Repository.RecordNotFoundException e) {
            // suppress/log exception
        }

        pr.MarkFresh(product);
        pr.ProcessChanges();
    }
}

Điều này dẫn đến việc có các dịch vụ xác định quy tắc miền thay vì chính lớp miền và bạn có các quy tắc nằm rải rác trên nhiều phần của mã.

Tôi đã đề cập đến nguyên tắc TellD Don'tAsk, vì như bạn có thể thấy rõ, dịch vụ cung cấp một hành động (nó sẽ lưu Producthoặc ném một ngoại lệ), nhưng bên trong phương thức bạn đang thao tác trên các đối tượng thông qua cách tiếp cận thủ tục.

Giải pháp rõ ràng là tạo ra một Domain.ProductCollectionlớp với một Add(Domain.Product)phương thức ProductWithSKUAlreadyExistsException, nhưng nó thiếu hiệu năng rất nhiều, bởi vì bạn sẽ cần lấy tất cả các Sản phẩm từ bộ lưu trữ dữ liệu để tìm ra mã, liệu một Sản phẩm có cùng SKU không là Sản phẩm bạn đang cố gắng thêm.

Làm thế nào để các bạn giải quyết vấn đề cụ thể này? Đây không thực sự là một vấn đề, tôi đã có lớp dịch vụ đại diện cho các quy tắc tên miền nhất định trong nhiều năm. Lớp dịch vụ thường cũng phục vụ các hoạt động miền phức tạp hơn, tôi chỉ đơn giản là tự hỏi liệu bạn có vấp phải một giải pháp tốt hơn, tập trung hơn trong sự nghiệp của mình không.


Tại sao phải kéo toàn bộ danh sách các tên khi bạn chỉ có thể yêu cầu một tên và dựa trên logic liệu có được tìm thấy hay không? Bạn đang nói với lớp kiên trì phải làm gì và không làm như thế nào miễn là có một thỏa thuận với giao diện.
JeffO

1
Khi sử dụng thuật ngữ Dịch vụ, chúng ta nên cố gắng hướng tới sự rõ ràng hơn, chẳng hạn như sự khác biệt giữa các dịch vụ miền trong lớp miền và dịch vụ ứng dụng trong lớp ứng dụng. Xem gorodinski.com/blog/2012/04/14/ từ
Erik Eidt

Bài viết tuyệt vời, @ErikEidt, cảm ơn bạn. Tôi đoán thiết kế của tôi không sai, nếu tôi có thể tin tưởng ông Gorodinski, ông cũng nói tương tự: Một giải pháp tốt hơn là yêu cầu một dịch vụ ứng dụng lấy thông tin theo yêu cầu của một thực thể, thiết lập hiệu quả môi trường thực thi và cung cấp nó cho thực thể và có cùng một đối tượng chống lại việc lưu trữ kho lưu trữ vào mô hình Miền trực tiếp như tôi, chủ yếu thông qua việc phá vỡ SRP.
Andy

Một trích dẫn từ tài liệu tham khảo "nói, đừng hỏi": "Nhưng cá nhân tôi, tôi không sử dụng nói-không-hỏi."
radarbob

Câu trả lời:


7

Xem xét lớp miền là không biết gì về sự kiên trì và không biết làm thế nào để lấy lại dữ liệu mà chỉ có cách thực hiện các thao tác trên chúng, nó thực sự không thể chạm vào các kho lưu trữ.

Tôi sẽ không đồng ý với phần này. Đặc biệt là câu cuối cùng.

Mặc dù đúng là tên miền nên không biết gì, nhưng nó biết rằng có "Bộ sưu tập các thực thể miền". Và có những quy tắc tên miền liên quan đến bộ sưu tập này nói chung. Độc đáo là một trong số họ. Và bởi vì việc triển khai logic thực tế phụ thuộc rất nhiều vào chế độ kiên trì cụ thể, nên phải có một số loại trừu tượng trong miền xác định cần phải có logic này.

Vì vậy, nó đơn giản như việc tạo một giao diện có thể truy vấn nếu tên đã tồn tại, sau đó được triển khai trong kho dữ liệu của bạn và được gọi bởi bất kỳ ai cần biết nếu tên đó là duy nhất.

Và tôi muốn nhấn mạnh rằng các kho lưu trữ là các dịch vụ DOMAIN. Họ là trừu tượng xung quanh sự kiên trì. Đó là việc thực hiện kho lưu trữ nên tách biệt với miền. Hoàn toàn không có gì sai với thực thể tên miền gọi một dịch vụ tên miền. Không có gì sai khi một thực thể có thể sử dụng kho lưu trữ để truy xuất thực thể khác hoặc truy xuất một số thông tin cụ thể, không thể lưu giữ trong bộ nhớ. Đây là một lý do tại sao Kho lưu trữ là khái niệm chính trong cuốn sách của Evans .


Cảm ơn bạn đã nhập. Kho lưu trữ có thể thực sự đại diện cho cái Domain.ProductCollectiontôi có trong đầu, xem xét chúng có trách nhiệm lấy các đối tượng từ lớp Miền không?
Andy

@DavidPacker Tôi không thực sự hiểu ý của bạn. Bởi vì không cần phải giữ tất cả các mục trong bộ nhớ. Việc triển khai phương thức "DoesNameExist" phải là (hầu hết có thể) truy vấn SQL ở phía lưu trữ dữ liệu.
phấn khích

Điều đó có nghĩa là gì, thay vì lưu trữ dữ liệu trong bộ sưu tập trong bộ nhớ, khi tôi muốn biết tất cả Sản phẩm tôi không cần lấy chúng từ đó, Domain.ProductCollectionnhưng hãy hỏi kho lưu trữ, tương tự với việc hỏi, liệu Domain.ProductCollectioncó chứa Sản phẩm với thay vào đó, đã thông qua SKU, lần nữa, yêu cầu kho lưu trữ (đây thực sự là ví dụ), thay vì lặp lại các sản phẩm được tải sẵn truy vấn cơ sở dữ liệu cơ bản. Tôi không có nghĩa là lưu trữ tất cả Sản phẩm trong bộ nhớ trừ khi tôi phải làm vậy, làm như vậy sẽ hoàn toàn vô nghĩa.
Andy

Điều này dẫn đến một câu hỏi khác, khi đó kho lưu trữ nên biết, liệu một thuộc tính có phải là duy nhất không? Tôi đã luôn triển khai các kho lưu trữ như các thành phần khá ngu ngốc, lưu những gì bạn vượt qua chúng để lưu và cố gắng truy xuất dữ liệu dựa trên các điều kiện đã thông qua và đưa quyết định vào các dịch vụ.
Andy

@DavidPacker Vâng, "kiến thức tên miền" nằm trong giao diện. Giao diện cho biết "miền cần chức năng này" và kho lưu trữ dữ liệu sau đó cung cấp chức năng đó. Nếu bạn có IProductRepositorygiao diện với DoesNameExist, rõ ràng phương thức nên làm là gì. Nhưng đảm bảo rằng Sản phẩm không thể có cùng tên với sản phẩm hiện có nên có trong miền ở đâu đó.
Euphoric

3

Bạn cần đọc Greg Young trên thiết lập xác nhận .

Câu trả lời ngắn gọn: trước khi bạn đi quá xa tổ chuột, bạn cần chắc chắn rằng bạn hiểu giá trị của yêu cầu từ quan điểm kinh doanh. Làm thế nào tốn kém, thực sự, để phát hiện và giảm thiểu sự trùng lặp, thay vì ngăn chặn nó?

Vấn đề với các yêu cầu của duy nhất là, đó là, rất thường xuyên có một lý do sâu xa hơn tại sao mọi người muốn chúng - Yves Reynhout

Câu trả lời dài hơn: Tôi đã thấy một menu các khả năng, nhưng tất cả chúng đều có sự đánh đổi.

Bạn có thể kiểm tra các bản sao trước khi gửi lệnh đến miền. Điều này có thể được thực hiện trong máy khách hoặc trong dịch vụ (ví dụ của bạn cho thấy kỹ thuật). Nếu bạn không hài lòng với logic rò rỉ ra khỏi lớp miền, bạn có thể đạt được cùng loại kết quả với a DomainService.

class Product {
    void register(SKU sku, DuplicationService skuLookup) {
        if (skuLookup.isKnownSku(sku) {
            throw ProductWithSKUAlreadyExistsException(...)
        }
        ...
    }
}

Tất nhiên, thực hiện theo cách này, việc triển khai Ded repeatationService sẽ cần phải biết một vài điều về cách tìm kiếm skus hiện có. Vì vậy, trong khi nó đẩy một số công việc trở lại miền, bạn vẫn phải đối mặt với các vấn đề cơ bản tương tự (cần một câu trả lời cho xác thực đã đặt, các vấn đề với điều kiện cuộc đua).

Bạn có thể thực hiện xác nhận trong lớp kiên trì của mình. Cơ sở dữ liệu quan hệ là thực sự tốt trong thiết lập xác nhận. Đặt một ràng buộc duy nhất trên cột sku của sản phẩm của bạn, và bạn tốt để đi. Ứng dụng chỉ lưu sản phẩm vào kho lưu trữ và bạn sẽ gặp phải một vi phạm ràng buộc sao lưu nếu có vấn đề. Vì vậy, mã ứng dụng có vẻ tốt và điều kiện cuộc đua của bạn đã bị loại bỏ, nhưng bạn đã có quy tắc "tên miền" bị rò rỉ.

Bạn có thể tạo một tập hợp riêng trong miền của bạn đại diện cho tập hợp các skus đã biết. Tôi có thể nghĩ về hai biến thể ở đây.

Một là một cái gì đó giống như một ProductCatalog; sản phẩm tồn tại ở một nơi khác, nhưng mối quan hệ giữa sản phẩm và skus được duy trì bởi một danh mục đảm bảo tính độc đáo của sku. Không phải điều này ngụ ý rằng các sản phẩm không có skus; skus được gán bởi một ProductCatalog (nếu bạn cần skus là duy nhất, bạn đạt được điều này bằng cách chỉ có một tổng hợp ProductCatalog duy nhất). Xem lại ngôn ngữ phổ biến với các chuyên gia tên miền của bạn - nếu điều đó tồn tại, đây cũng có thể là phương pháp phù hợp.

Một thay thế là một cái gì đó giống như một dịch vụ đặt phòng sku. Cơ chế cơ bản là như nhau: một tổng hợp biết về tất cả các skus, vì vậy có thể ngăn chặn sự ra đời của các bản sao. Nhưng cơ chế hơi khác một chút: bạn có được hợp đồng thuê sku trước khi giao nó cho sản phẩm; Khi tạo sản phẩm, bạn chuyển hợp đồng thuê cho sku. Vẫn còn một điều kiện chạy đua (các tập hợp khác nhau, do đó các giao dịch khác nhau), nhưng nó có một hương vị khác với nó. Nhược điểm thực sự ở đây là bạn đang chiếu vào mô hình miền một dịch vụ cho thuê mà không thực sự có sự biện minh trong ngôn ngữ tên miền.

Bạn có thể kéo tất cả các thực thể sản phẩm vào một tập hợp duy nhất - tức là danh mục sản phẩm được mô tả ở trên. Bạn hoàn toàn có được sự độc đáo của skus khi bạn làm điều này, nhưng chi phí là sự tranh chấp thêm, sửa đổi bất kỳ sản phẩm nào thực sự có nghĩa là sửa đổi toàn bộ danh mục.

Tôi không thích phải rút tất cả SKU sản phẩm ra khỏi cơ sở dữ liệu để thực hiện thao tác trong bộ nhớ.

Có lẽ bạn không cần. Nếu bạn kiểm tra sku của mình bằng bộ lọc Bloom , bạn có thể khám phá nhiều skus độc đáo mà không cần tải bộ nào cả.

Nếu trường hợp sử dụng của bạn cho phép bạn tùy ý về việc bạn từ chối, bạn có thể loại bỏ tất cả các kết quả dương tính giả (không phải là vấn đề lớn nếu bạn cho phép khách hàng kiểm tra skus mà họ đề xuất trước khi gửi lệnh). Điều đó sẽ cho phép bạn tránh tải tập hợp vào bộ nhớ.

(Nếu bạn muốn được chấp nhận nhiều hơn, bạn có thể cân nhắc việc lười tải skus trong trường hợp trận đấu trong bộ lọc nở; đôi khi bạn vẫn có nguy cơ tải tất cả các skus vào bộ nhớ, nhưng đó không phải là trường hợp phổ biến nếu bạn cho phép mã máy khách để kiểm tra lỗi trước khi gửi).


Tôi không thích phải rút tất cả SKU sản phẩm ra khỏi cơ sở dữ liệu để thực hiện thao tác trong bộ nhớ. Đó là giải pháp rõ ràng tôi đề xuất trong câu hỏi ban đầu và đặt câu hỏi về hiệu suất của nó. Ngoài ra, IMO, dựa vào ràng buộc trong DB của bạn, chịu trách nhiệm về tính duy nhất, là không tốt. Nếu bạn đã chuyển sang một công cụ cơ sở dữ liệu mới và bằng cách nào đó, ràng buộc duy nhất bị mất trong quá trình chuyển đổi, bạn đã bị hỏng mã, vì thông tin cơ sở dữ liệu ở đó trước đó không còn nữa. Dù sao cũng cảm ơn bạn đã liên kết, đọc thú vị.
Andy

Có lẽ một bộ lọc Bloom (xem chỉnh sửa).
VoiceOfUnreason
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.