DDD đáp ứng OOP: Làm thế nào để thực hiện một kho lưu trữ hướng đối tượng?


12

Một triển khai điển hình của kho lưu trữ DDD trông không giống OO, ví dụ như một save()phương thức:

package com.example.domain;

public class Product {  /* public attributes for brevity */
    public String name;
    public Double price;
}

public interface ProductRepo {
    void save(Product product);
} 

Phần cơ sở hạ tầng:

package com.example.infrastructure;
// imports...

public class JdbcProductRepo implements ProductRepo {
    private JdbcTemplate = ...

    public void save(Product product) {
        JdbcTemplate.update("INSERT INTO product (name, price) VALUES (?, ?)", 
            product.name, product.price);
    }
} 

Một giao diện như vậy hy vọng sẽ Productlà một mô hình thiếu máu, ít nhất là với getters.

Mặt khác, OOP nói rằng một Productđối tượng nên biết cách tự cứu mình.

package com.example.domain;

public class Product {
    private String name;
    private Double price;

    void save() {
        // save the product
        // ???
    }
}

Vấn đề là, khi Productbiết cách tự lưu, điều đó có nghĩa là mã cơ sở hạ tầng không tách rời khỏi mã miền.

Có lẽ chúng ta có thể ủy thác việc tiết kiệm cho một đối tượng khác:

package com.example.domain;

public class Product {
    private String name;
    private Double price;

    void save(Storage storage) {
        storage
            .with("name", this.name)
            .with("price", this.price)
            .save();
    }
}

public interface Storage {
    Storage with(String name, Object value);
    void save();
}

Phần cơ sở hạ tầng:

package com.example.infrastructure;
// imports...

public class JdbcProductRepo implements ProductRepo {        
    public void save(Product product) {
        product.save(new JdbcStorage());
    }
}

class JdbcStorage implements Storage {
    private final JdbcTemplate = ...
    private final Map<String, Object> attrs = new HashMap<>();

    private final String tableName;

    public JdbcStorage(String tableName) {
        this.tableName = tableName;
    }

    public Storage with(String name, Object value) {
        attrs.put(name, value);
    }
    public void save() {
        JdbcTemplate.update("INSERT INTO " + tableName + " (name, price) VALUES (?, ?)", 
            attrs.get("name"), attrs.get("price"));
    }
}

Cách tiếp cận tốt nhất để đạt được điều này là gì? Có thể thực hiện một kho lưu trữ hướng đối tượng?


6
OOP nói rằng một đối tượng Sản phẩm nên biết cách tự cứu mình - Tôi không chắc điều đó thực sự đúng ... Bản thân OOP không thực sự cho rằng đó là vấn đề về thiết kế / mẫu (đó là vấn đề DDD / sao cũng được
-Sử

1
Hãy nhớ rằng trong bối cảnh của OOP, nó đang nói về các đối tượng. Chỉ là đối tượng, không kiên trì dữ liệu. Tuyên bố của bạn chỉ ra rằng trạng thái của một đối tượng không nên được quản lý bên ngoài mà tôi đồng ý. Một kho lưu trữ chịu trách nhiệm tải / lưu từ một số lớp kiên trì (nằm ngoài lãnh địa của OOP). Các thuộc tính và phương thức lớp nên duy trì tính toàn vẹn của riêng chúng, vâng, nhưng điều này không có nghĩa là một đối tượng khác không thể chịu trách nhiệm cho việc duy trì trạng thái. Và, getters và setters là để đảm bảo tính toàn vẹn của dữ liệu đến / đi của đối tượng.
jleach

1
"điều này không có nghĩa là một đối tượng khác không thể chịu trách nhiệm cho việc duy trì nhà nước." - Tôi không nói thế. Tuyên bố quan trọng là, một đối tượng nên hoạt động . Nó có nghĩa là đối tượng (và không ai khác) có thể ủy thác thao tác này cho đối tượng khác, nhưng không phải là cách khác: không đối tượng nào chỉ nên thu thập thông tin từ một đối tượng thụ động để xử lý hoạt động ích kỷ của chính mình (như một repo sẽ làm với getters) . Tôi đã cố gắng thực hiện phương pháp này trong đoạn trích ở trên.
ttulka

1
@jleach Bạn nói đúng, việc hủy bỏ OOP của chúng tôi là khác nhau, đối với tôi getters + setters hoàn toàn không phải là OOP, nếu không thì câu hỏi của tôi không có ý nghĩa gì. Dù sao cũng cảm ơn bạn :-)
ttulka

1
Đây là một bài viết về quan điểm của tôi: martinfowler.com/bliki/AnemiaDomainModel.html Tôi không làm phiền mô hình thiếu máu trong mọi trường hợp, ví dụ đó là một chiến lược tốt cho lập trình chức năng. Chỉ là không OOP.
ttulka

Câu trả lời:


7

Bạn đã viết

Mặt khác, OOP nói rằng một đối tượng Sản phẩm nên biết cách tự lưu

và trong một bình luận.

... phải chịu trách nhiệm cho tất cả các hoạt động được thực hiện với nó

Đây là một sự hiểu lầm phổ biến. Productlà một đối tượng miền, do đó, nó phải chịu trách nhiệm cho các hoạt động miền liên quan đến một đối tượng sản phẩm duy nhất , không hơn không kém - vì vậy chắc chắn không phải cho tất cả các hoạt động. Thông thường sự kiên trì không được xem là một hoạt động tên miền. Hoàn toàn ngược lại, trong các ứng dụng doanh nghiệp, không có gì lạ khi cố gắng đạt được sự thiếu hiểu biết dai dẳng trong mô hình miền (ít nhất là ở một mức độ nhất định) và giữ cơ học bền vững trong một lớp kho lưu trữ riêng biệt là một giải pháp phổ biến cho việc này. "DDD" là một kỹ thuật nhằm vào loại ứng dụng này.

Vì vậy, những gì có thể là một hoạt động miền hợp lý cho một Product? Điều này thực sự phụ thuộc vào bối cảnh miền của hệ thống ứng dụng. Nếu hệ thống là một hệ thống nhỏ và chỉ hỗ trợ các hoạt động CRUD độc quyền, thì thực sự, một hệ thống Productcó thể vẫn khá "thiếu máu" như trong ví dụ của bạn. Đối với loại ứng dụng như vậy, có thể gây tranh cãi nếu việc đưa các hoạt động cơ sở dữ liệu vào một lớp repo riêng hoặc sử dụng DDD hoàn toàn có giá trị.

Tuy nhiên, ngay khi ứng dụng của bạn hỗ trợ các hoạt động kinh doanh thực tế, như mua hoặc bán sản phẩm, giữ chúng trong kho và quản lý chúng, hoặc tính thuế cho chúng, việc bạn bắt đầu khám phá các hoạt động có thể được đặt một cách hợp lý trong một Productlớp là điều khá phổ biến . Ví dụ, có thể có một hoạt động CalcTotalPrice(int noOfItems)tính giá cho `n mặt hàng của một sản phẩm nhất định khi thực hiện giảm giá khối lượng.

Vì vậy, trong ngắn hạn, khi bạn thiết kế các lớp, bạn cần suy nghĩ về bối cảnh của mình, trong đó có năm thế giới của Joel Spolsky , và nếu hệ thống chứa đủ logic miền thì DDD sẽ có ích. Nếu câu trả lời là có, rất có thể bạn sẽ không kết thúc với một mô hình thiếu máu chỉ vì bạn giữ các cơ chế kiên trì ra khỏi các lớp miền.


Quan điểm của bạn nghe rất hợp lý với tôi. Vì vậy, sản phẩm trở thành cấu trúc dữ liệu thiếu máu khi vượt qua biên giới của bối cảnh cấu trúc dữ liệu thiếu máu (cơ sở dữ liệu) và kho lưu trữ là một cổng. Nhưng điều này vẫn có nghĩa là tôi phải cung cấp quyền truy cập vào cấu trúc bên trong của đối tượng thông qua getter và setters, sau đó trở thành một phần của API của nó và có thể dễ dàng bị lạm dụng bởi các mã khác, không liên quan gì đến sự kiên trì. Có một thực hành tốt làm thế nào để tránh điều này? Cảm ơn bạn!
ttulka

"Nhưng điều này vẫn có nghĩa là tôi phải cung cấp quyền truy cập vào cấu trúc bên trong của đối tượng thông qua getter và setters" - không thể. Trạng thái bên trong của một đối tượng miền không biết gì về tính bền vững thường được cung cấp riêng bởi một tập hợp các thuộc tính liên quan đến miền. Đối với các thuộc tính này, getters và setters (hoặc khởi tạo hàm tạo) phải tồn tại, nếu không thì không thể thực hiện thao tác miền "thú vị" nào. Trong một số khung công tác, cũng có các tính năng bền vững có sẵn cho phép duy trì các thuộc tính riêng tư bằng phản xạ, do đó việc đóng gói chỉ bị phá vỡ đối với cơ chế này, không phải cho "mã khác".
Doc Brown

1
Tôi đồng ý rằng sự kiên trì thường không phải là một phần của hoạt động miền, tuy nhiên nó phải là một phần của hoạt động miền "thực" bên trong đối tượng cần nó. Ví dụ Account.transfer(amount)nên kiên trì chuyển tiền. Làm thế nào nó là trách nhiệm của đối tượng, không phải một số thực thể bên ngoài. Hiển thị đối tượng, mặt khác thường là một lĩnh vực hoạt động! Các yêu cầu thường mô tả rất chi tiết cách thức công cụ nên nhìn. Nó là một phần của ngôn ngữ giữa các thành viên dự án, kinh doanh hoặc cách khác.
Robert Bräutigam

@ RobertBräutigam: cổ điển Account.transferthường liên quan đến hai đối tượng tài khoản và một đơn vị đối tượng công việc. Hoạt động duy trì giao dịch sau đó có thể là một phần của hoạt động sau (chuyển đổi với các cuộc gọi đến các repos liên quan), do đó, nó nằm ngoài phương thức "chuyển tiền". Theo cách đó, Accountcó thể ở lại không biết gì. Tôi không nói rằng điều này nhất thiết phải tốt hơn giải pháp được cho là của bạn, nhưng bạn cũng chỉ là một trong một số cách tiếp cận có thể.
Doc Brown

1
@ RobertBräutigam Khá chắc chắn rằng bạn đang suy nghĩ quá nhiều về mối quan hệ giữa đối tượng và bảng. Hãy nghĩ về đối tượng như có một trạng thái cho chính nó, tất cả trong bộ nhớ. Sau khi thực hiện chuyển khoản trong các đối tượng tài khoản của bạn, bạn sẽ bị bỏ lại với các đối tượng có trạng thái mới. Đó là những gì bạn muốn duy trì và may mắn thay, các đối tượng tài khoản cung cấp một cách để cho bạn biết về trạng thái của họ. Điều đó không có nghĩa là trạng thái của họ phải bằng với các bảng trong cơ sở dữ liệu - tức là số tiền được chuyển có thể là một đối tượng tiền có chứa số tiền thô và tiền tệ.
Steve Chamaillard

5

Thực hành lý thuyết trumps.

Kinh nghiệm dạy chúng ta rằng Product.Save () dẫn đến nhiều vấn đề. Để giải quyết những vấn đề đó, chúng tôi đã phát minh ra mô hình kho lưu trữ.

Chắc chắn rằng nó phá vỡ quy tắc OOP của việc ẩn dữ liệu sản phẩm. Nhưng nó hoạt động tốt.

Thật khó khăn hơn nhiều để tạo ra một bộ quy tắc nhất quán bao gồm mọi thứ so với việc tạo ra một số quy tắc tốt chung có ngoại lệ.


3

DDD gặp OOP

Nó giúp ghi nhớ rằng không có ý định căng thẳng giữa hai ý tưởng này - đối tượng giá trị, tập hợp, kho lưu trữ là một mảng các mẫu được sử dụng là điều mà một số người coi là OOP thực hiện đúng.

Mặt khác, OOP nói rằng một đối tượng Sản phẩm nên biết cách tự lưu.

Không phải vậy. Các đối tượng gói gọn các cấu trúc dữ liệu của riêng họ. Đại diện trong bộ nhớ của Sản phẩm chịu trách nhiệm thể hiện các hành vi của sản phẩm (bất kể chúng là gì); nhưng lưu trữ liên tục ở đằng kia (đằng sau kho lưu trữ) và có công việc riêng để làm.

Không cần phải có một số cách để sao chép dữ liệu giữa biểu diễn trong bộ nhớ của cơ sở dữ liệu và lưu trữ liên tục. Ở ranh giới , mọi thứ có xu hướng khá nguyên thủy.

Về cơ bản, chỉ viết cơ sở dữ liệu không đặc biệt hữu ích và tương đương trong bộ nhớ của chúng không hữu ích hơn loại "tồn tại". Không có điểm nào trong việc đưa thông tin vào một Productđối tượng nếu bạn sẽ không bao giờ lấy thông tin đó ra. Bạn không nhất thiết phải sử dụng "getters" - bạn không cố gắng chia sẻ cấu trúc dữ liệu sản phẩm và chắc chắn bạn không nên chia sẻ quyền truy cập có thể thay đổi vào đại diện bên trong của Sản phẩm.

Có lẽ chúng ta có thể ủy thác việc tiết kiệm cho một đối tượng khác:

Điều đó chắc chắn hoạt động - lưu trữ liên tục của bạn có hiệu quả trở thành một cuộc gọi lại. Tôi có thể sẽ làm cho giao diện đơn giản hơn:

interface ProductStorage {
    onProduct(String name, double price);
}

đi được ghép giữa tình trạng đại diện bộ nhớ và cơ chế lưu trữ, bởi vì thông tin cần để có được từ đây đến đó (và ngược lại). Thay đổi thông tin sẽ được chia sẻ sẽ tác động đến cả hai đầu của cuộc trò chuyện. Vì vậy, chúng tôi cũng có thể làm cho rõ ràng nơi chúng ta có thể.

Cách tiếp cận này - truyền dữ liệu qua các cuộc gọi lại, đã đóng một vai trò quan trọng trong việc phát triển các giả trong TDD .

Lưu ý rằng việc chuyển thông tin cho cuộc gọi lại có tất cả các hạn chế giống như trả lại thông tin từ truy vấn - bạn không nên chuyển qua các bản sao có thể thay đổi của cấu trúc dữ liệu của mình.

Cách tiếp cận này hơi trái ngược với những gì Evans mô tả trong Sách xanh, trong đó việc trả lại dữ liệu qua truy vấn là cách thông thường để thực hiện và các đối tượng miền được thiết kế đặc biệt để tránh trộn lẫn vào "mối quan tâm dai dẳng".

Tôi hiểu DDD là một kỹ thuật OOP và vì vậy tôi muốn hiểu đầy đủ rằng dường như mâu thuẫn.

Một điều cần ghi nhớ - Cuốn sách màu xanh đã được viết cách đây mười lăm năm, khi Java 1.4 đi lang thang trên trái đất. Đặc biệt, cuốn sách ra trước Java Generics - chúng tôi có rất nhiều kỹ thuật sẵn có đối với chúng tôi bây giờ thì khi Evans đã phát triển ý tưởng của mình.


2
Cũng đáng đề cập: "tự lưu" sẽ luôn yêu cầu tương tác với các đối tượng khác (đối tượng hệ thống tệp hoặc cơ sở dữ liệu hoặc dịch vụ web từ xa, một số trong số này có thể yêu cầu phiên được thiết lập để kiểm soát truy cập). Vì vậy, một đối tượng như vậy sẽ không tự đứng vững và độc lập. Do đó, OOP không thể yêu cầu điều này, vì mục đích của nó là đóng gói đối tượng và giảm khớp nối.
Barshe

Cảm ơn bạn cho một câu trả lời tuyệt vời. Đầu tiên, tôi thiết kế Storagegiao diện giống như cách bạn đã làm, sau đó tôi xem xét tính khớp nối cao và thay đổi nó. Nhưng bạn đã đúng, dù sao cũng có một khớp nối không thể tránh khỏi, vậy tại sao không làm cho nó rõ ràng hơn.
ttulka

1
"Cách tiếp cận này hơi trái ngược với những gì Evans mô tả trong Sách xanh" - vì vậy có một chút căng thẳng :-) Đó thực sự là vấn đề của tôi, tôi hiểu DDD là một kỹ thuật OOP và vì vậy tôi muốn hoàn toàn hiểu rằng dường như mâu thuẫn.
ttulka

1
Theo kinh nghiệm của tôi, mỗi thứ trong số này (OOP nói chung, DDD, TDD, từ viết tắt của bạn) đều nghe rất hay và tốt cho bản thân chúng, nhưng bất cứ khi nào thực hiện "thế giới thực", luôn có sự đánh đổi hoặc chủ nghĩa lý tưởng ít hơn phải làm cho nó hoạt động.
jleach

Tôi không đồng ý với khái niệm rằng sự kiên trì (và cách trình bày) bằng cách nào đó "đặc biệt". Họ không phải. Họ nên là một phần của mô hình để mở rộng nhu cầu yêu cầu. Không cần phải có ranh giới nhân tạo (dựa trên dữ liệu) bên trong ứng dụng, trừ khi có những yêu cầu thực tế ngược lại.
Robert Bräutigam

1

Quan sát rất tốt, tôi hoàn toàn đồng ý với bạn về chúng. Dưới đây là bài nói chuyện của tôi (chỉ sửa: slide) về chính xác chủ đề này: Thiết kế hướng tên miền hướng đối tượng .

Câu trả lời ngắn gọn: không. Không nên có một đối tượng trong ứng dụng của bạn hoàn toàn là kỹ thuật và không liên quan đến tên miền. Điều đó giống như thực hiện khung đăng nhập trong một ứng dụng kế toán.

StorageVí dụ giao diện của bạn là một ví dụ tuyệt vời, giả sử Storagesau đó được coi là một số khung bên ngoài, ngay cả khi bạn viết nó.

Ngoài ra, save()trong một đối tượng chỉ nên được cho phép nếu đó là một phần của miền ("ngôn ngữ"). Ví dụ, tôi không nên yêu cầu "lưu" một cách rõ ràng Accountsau khi tôi gọi transfer(amount). Tôi nên kỳ vọng rằng chức năng kinh doanh transfer()sẽ tiếp tục chuyển khoản của tôi.

Nói chung, tôi nghĩ ý tưởng của DDD là tốt. Sử dụng ngôn ngữ phổ biến, thực hiện tên miền với cuộc trò chuyện, bối cảnh bị ràng buộc, v.v. Tuy nhiên, các khối xây dựng cần phải đại tu nghiêm túc nếu tương thích với hướng đối tượng. Xem các sàn liên kết để biết chi tiết.


Là nói chuyện của bạn ở đâu đó để xem? (Tôi thấy chỉ là các slide dưới liên kết). Cảm ơn!
ttulka

Tôi chỉ có một bản ghi âm tiếng Đức về buổi nói chuyện, ở đây: javadevguy.wordpress.com/2018/11/26/iêu
Robert Bräutigam

Nói chuyện tuyệt vời! (May mắn thay tôi nói tiếng Đức). Tôi nghĩ rằng toàn bộ blog của bạn đáng đọc ... Cảm ơn bạn đã làm việc của bạn!
ttulka

Thanh trượt rất sâu sắc Robert. Tôi thấy nó rất minh họa nhưng tôi có cảm giác rằng cuối cùng, nhiều giải pháp được đề cập là không phá vỡ đóng gói và LoD dựa trên việc cung cấp nhiều khả năng phản hồi cho đối tượng miền: in, tuần tự hóa, định dạng giao diện người dùng, v.v. t có làm tăng sự ghép nối giữa miền và kỹ thuật (chi tiết triển khai) không? Ví dụ: AccountNumber được kết hợp với API Wicket của Apache. Hoặc Tài khoản với đối tượng Json là gì? Bạn có nghĩ rằng đó là một khớp nối đáng để có?
Laiv

@Laiv Ngữ pháp của câu hỏi của bạn cho thấy có vấn đề gì với việc sử dụng công nghệ để thực hiện các chức năng kinh doanh? Chúng ta hãy giải thích nó theo cách này: Không phải sự kết hợp giữa miền và công nghệ là vấn đề, đó là sự kết hợp giữa các mức độ trừu tượng khác nhau. Ví dụ AccountNumber nên biết rằng nó có thể được biểu diễn dưới dạng a TextField. Nếu những người khác (như "Chế độ xem") sẽ biết điều này, thì đó là khớp nối không nên tồn tại, bởi vì thành phần đó sẽ cần phải biết những gì AccountNumberbao gồm, tức là bên trong.
Robert Bräutigam

1

Có lẽ chúng ta có thể ủy thác việc tiết kiệm cho một đối tượng khác

Tránh truyền bá kiến ​​thức về các lĩnh vực không cần thiết. Càng nhiều điều biết về một lĩnh vực riêng lẻ, càng khó để thêm hoặc xóa một trường:

public class Product {
    private String name;
    private Double price;

    void save(Storage storage) {
        storage.save( toString() );
    }
}

Ở đây, sản phẩm không có ý tưởng nếu bạn lưu vào tệp nhật ký hoặc cơ sở dữ liệu hoặc cả hai. Ở đây phương thức lưu không có ý tưởng nếu bạn có 4 hoặc 40 trường. Đó là kết nối lỏng lẻo. Đó là một điều tốt.

Tất nhiên đây chỉ là một ví dụ về cách bạn có thể đạt được mục tiêu này. Nếu bạn không thích xây dựng và phân tích chuỗi để sử dụng làm DTO của mình, bạn cũng có thể sử dụng bộ sưu tập. LinkedHashMaplà một yêu thích cũ của tôi vì nó giữ trật tự và nó toString () trông tốt trong một tệp nhật ký.

Tuy nhiên, bạn làm điều đó, xin vui lòng không truyền bá kiến ​​thức về các lĩnh vực xung quanh. Đây là một hình thức khớp nối mà mọi người thường bỏ qua cho đến khi nó đến muộn. Tôi muốn có ít thứ để tĩnh biết đối tượng của tôi có bao nhiêu trường càng tốt. Theo cách đó, việc thêm một trường không liên quan đến nhiều chỉnh sửa ở nhiều nơi.


Đây thực tế là mã tôi đã đăng trong câu hỏi của tôi, phải không? Tôi đã sử dụng một Map, bạn đề xuất một Stringhoặc a List. Nhưng, như @VoiceOfUnreason đã đề cập trong câu trả lời của mình, khớp nối vẫn còn đó, chỉ là không rõ ràng. Vẫn không cần biết cấu trúc dữ liệu của sản phẩm để lưu cả trong cơ sở dữ liệu hoặc tệp nhật ký, ít nhất là khi đọc lại dưới dạng đối tượng.
ttulka

Tôi đã thay đổi phương thức lưu nhưng nếu không thì nó cũng giống như vậy. Sự khác biệt là khớp nối không còn tĩnh cho phép thêm các trường mới mà không buộc phải thay đổi mã cho hệ thống lưu trữ. Điều đó làm cho hệ thống lưu trữ có thể tái sử dụng trên nhiều sản phẩm khác nhau. Nó chỉ buộc bạn phải làm những việc có chút không tự nhiên như biến một đôi thành một chuỗi và trở lại thành một đôi. Nhưng điều đó cũng có thể được giải quyết nếu nó thực sự là một vấn đề.
candied_orange


Nhưng như tôi đã nói, tôi thấy khớp nối vẫn còn đó (bằng cách phân tích cú pháp), chỉ khi không tĩnh (rõ ràng) mang lại nhược điểm là không thể được kiểm tra bởi trình biên dịch và do đó dễ bị lỗi hơn. Đây Storagelà một phần của miền (cũng như giao diện kho lưu trữ) và tạo ra một API bền bỉ như vậy. Khi thay đổi, tốt hơn là thông báo cho khách hàng trong thời gian biên dịch, vì dù sao họ cũng phải phản ứng để không bị hỏng trong thời gian chạy.
ttulka

Đó là một quan niệm sai lầm. Trình biên dịch không thể kiểm tra tệp nhật ký hoặc DB. Tất cả việc kiểm tra là nếu một tệp mã phù hợp với tệp mã khác không được đảm bảo phù hợp với tệp nhật ký hoặc DB.
candied_orange

0

Có một sự thay thế cho các mẫu đã được đề cập. Mẫu Memento là tuyệt vời để đóng gói trạng thái bên trong của một đối tượng miền. Đối tượng memento đại diện cho một ảnh chụp nhanh của trạng thái công khai đối tượng miền. Đối tượng miền biết cách tạo trạng thái công khai này từ trạng thái bên trong của nó và ngược lại. Một kho lưu trữ sau đó chỉ hoạt động với sự đại diện công khai của nhà nước. Cùng với đó, việc thực hiện nội bộ được tách rời khỏi bất kỳ chi tiết cụ thể nào và nó chỉ phải duy trì hợp đồng công khai. Ngoài ra, đối tượng miền của bạn không để lộ bất kỳ getters nào thực sự sẽ làm cho nó một chút thiếu máu.

Để biết thêm về chủ đề này, tôi giới thiệu cuốn sách tuyệt vời: "Các mô hình, nguyên tắc và thực tiễn của thiết kế hướng tên miền" của Scott Millett và Nick Tune

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.