Tìm gốc tổng hợp DDD


10

Hãy chơi trò chơi yêu thích của mọi người, tìm Root tổng hợp. Chúng ta hãy sử dụng miền vấn đề Khách hàng / Đơn hàng / Đơn hàng / Sản phẩm chuẩn. Theo truyền thống, Khách hàng, đơn hàng và sản phẩm là AR với OrderLines là các thực thể trong Đơn hàng. Logic đằng sau điều này là bạn cần xác định khách hàng, đơn hàng và sản phẩm, nhưng OrderLine sẽ không tồn tại nếu không có Đơn hàng. Vì vậy, trong miền vấn đề của chúng tôi, chúng tôi có một quy tắc kinh doanh nói rằng Khách hàng chỉ có thể có một đơn hàng chưa được gửi tại một thời điểm.

Điều đó có di chuyển các đơn đặt hàng dưới gốc tổng hợp khách hàng? Tôi nghĩ rằng nó làm. Nhưng khi làm như vậy, điều đó làm cho AR của Khách hàng khá lớn và gặp phải các vấn đề tương tranh sau này.

Hoặc, điều gì sẽ xảy ra nếu chúng ta có một quy tắc kinh doanh nói rằng khách hàng chỉ có thể đặt hàng một sản phẩm cụ thể một lần trong đời. Đây là nhiều bằng chứng yêu cầu Khách hàng sở hữu Đơn hàng.

Nhưng khi nói đến vận chuyển, họ thực hiện tất cả các hành động của mình trên Đơn hàng chứ không phải khách hàng. Thật là ngu ngốc khi phải tải lên toàn bộ khách hàng để đánh dấu một Đơn hàng riêng lẻ được giao.

Đây là những gì tôi đang đề xuất:

class Customer
{
    public Guid Id {get;set;}
    public string Name { get; set; }
    public Address Address { get; set; }
    public IEnumerable<Order> Orders { get; set; }
    public void PlaceOrder(ThingsInTheOrder thingsInTheOrder)
    {
        // Make sure there aren't any pending orders already.
        // Make sure they aren't ordering a Widget if they've already ordered a Widget in the past.
        // Create an Order object and add it to the collection.  Raise a domain event to trigger emails and other stuff
    }
}

class Order
{
    public Guid Id { get; set; }
    public IEnumerable<OrderLine> OrderLines { get; set; }
    public ShippingData {get;set;}
    public void Ship(ShippedByPerson shippedByPerson, string trackingCode)
    {
         // Create a new ShippingData object and assign it from the data passed in.  
         // Publish a domain event
    }
}

Mối quan tâm lớn nhất của tôi là vấn đề tương tranh và thực tế là chính Lệnh có các đặc điểm của một gốc tổng hợp.

Câu trả lời:


12

Phiên bản ngắn

Lý do của DDD là các Đối tượng Miền là các trừu tượng sẽ đáp ứng các yêu cầu miền chức năng của bạn - nếu Đối tượng Miền không thể dễ dàng thực hiện các yêu cầu đó, điều đó cho thấy bạn có thể đang sử dụng trừu tượng hóa sai.

Đặt tên các đối tượng miền bằng cách sử dụng Danh từ thực thể có thể dẫn đến các đối tượng đó trở nên gắn kết chặt chẽ với nhau và trở thành đối tượng "thần" cồng kềnh và họ có thể đưa ra các vấn đề như câu hỏi trong câu hỏi này như "Nơi nào là nơi thích hợp để đặt Phương thức tạo hay hơn? ".

Để giúp dễ dàng xác định Root tổng hợp 'đúng', hãy xem xét một cách tiếp cận khác trong đó Đối tượng miền dựa trên các yêu cầu nghiệp vụ cấp cao chức năng - tức là chọn các danh từ ám chỉ các yêu cầu chức năng và / hoặc hành vi mà người dùng hệ thống cần biểu diễn.


Phiên bản dài

DDD là một cách tiếp cận với OO Design nhằm tạo ra một biểu đồ các Đối tượng miền trong Lớp nghiệp vụ trong hệ thống của bạn - Các đối tượng miền chịu trách nhiệm đáp ứng các yêu cầu nghiệp vụ cấp cao của bạn và lý tưởng nhất là có thể dựa vào Lớp dữ liệu cho những thứ như hiệu suất và tính toàn vẹn của kho dữ liệu liên tục bên dưới.

Một cách khác để xem xét nó có thể là các gạch đầu dòng trong danh sách này

  • Danh từ thực thể thường đề xuất các thuộc tính dữ liệu.
  • Danh từ tên miền nên đề xuất hành vi
  • Mô hình DDD và OO liên quan đến trừu tượng dựa trên các yêu cầu chức năng và logic kinh doanh / miền cốt lõi.
  • Lớp Logic nghiệp vụ chịu trách nhiệm đáp ứng các yêu cầu miền cấp cao

Một trong những quan niệm sai lầm phổ biến liên quan đến DDD là Đối tượng miền phải dựa trên một số "vật" trong thế giới thực (nghĩa là một số danh từ mà bạn có thể chỉ ra trong thế giới thực, được gán cho tất cả các loại dữ liệu / thuộc tính), tuy nhiên dữ liệu / thuộc tính của những thứ trong thế giới thực không nhất thiết phải là điểm khởi đầu tốt khi cố gắng đưa ra các yêu cầu chức năng.

Tất nhiên, Business Logic nên sử dụng dữ liệu này, nhưng bản thân các Đối tượng Miền phải là trừu tượng đại diện cho các yêu cầu và hành vi của Miền chức năng.

Ví dụ; các danh từ như Orderhoặc Customerkhông ngụ ý bất kỳ hành vi nào, và do đó nói chung là trừu tượng không có ích để đại diện cho logic kinh doanh và Đối tượng miền.

Khi tìm kiếm các loại trừu tượng có thể hữu ích để biểu diễn Logic nghiệp vụ, hãy xem xét các yêu cầu điển hình mà bạn có thể mong đợi một hệ thống đáp ứng:

  • Là một nhân viên bán hàng, tôi muốn tạo Đơn hàng cho một khách hàng mới để tôi có thể tạo hóa đơn cho các Sản phẩm được bán với Giá và Số lượng của họ.
  • Với tư cách là Cố vấn dịch vụ khách hàng, tôi muốn hủy Đơn đặt hàng đang chờ xử lý để Đơn hàng không được Nhà điều hành kho thực hiện.
  • Với tư cách là Cố vấn dịch vụ khách hàng, tôi muốn trả lại một dòng đơn hàng để có thể điều chỉnh Sản phẩm thành hàng tồn kho và Thanh toán được hoàn lại thông qua phương thức Thanh toán ban đầu của Khách hàng.
  • Là Nhà điều hành kho, tôi muốn xem tất cả các Sản phẩm trên Đơn hàng đang chờ xử lý và thông tin Giao hàng để tôi có thể Chọn sản phẩm và gửi chúng qua Chuyển phát nhanh.
  • Vân vân.

Mô hình hóa các yêu cầu miền với phương pháp DDD

Dựa trên danh sách trên, hãy xem xét một số Đối tượng Miền tiềm năng cho hệ thống Đơn hàng như vậy:

SalesOrderCheckout
PendingOrdersStream
WarehouseOrderDespatcher
OrderRefundProcessor 

Là các đối tượng miền, chúng đại diện cho các khái niệm trừu tượng có quyền sở hữu các yêu cầu miền hành vi khác nhau; thực sự danh từ của họ gợi ý mạnh mẽ về (các) yêu cầu chức năng cụ thể mà họ đáp ứng.

(Có thể có thêm cơ sở hạ tầng trong đó, chẳng hạn như EventMediatorđể chuyển thông báo cho người quan sát muốn biết khi nào đơn hàng mới được tạo hoặc khi đơn hàng được giao, v.v.).

Ví dụ: SalesOrderCheckoutcó thể cần xử lý dữ liệu về Khách hàng, Giao hàng và Sản phẩm, tuy nhiên không liên quan đến bất kỳ điều gì liên quan đến hành vi đối với các đơn đặt hàng vận chuyển, sắp xếp các đơn đặt hàng đang chờ xử lý hoặc hoàn lại tiền.

Để SalesOrderCheckoutthực hiện các yêu cầu miền của nó bao gồm thực thi các quy tắc kinh doanh đó như ngăn khách hàng đặt mua quá nhiều mặt hàng, có thể chạy một số xác thực và có thể tăng thông báo cho các bộ phận khác của hệ thống - nó có thể thực hiện tất cả những điều đó mà không nhất thiết phải phụ thuộc vào bất kỳ của các đối tượng khác.

DDD sử dụng Danh từ thực thể để thể hiện các Đối tượng Miền

Có một số nguy hiểm tiềm ẩn khi điều trị các danh từ đơn giản như Order, CustomerProductnhư Domain Objects; trong số những vấn đề đó có những vấn đề bạn ám chỉ trong câu hỏi:

  • Nếu một phương thức xử lý một Order, a Customervà a Product, thì đối tượng miền nào thuộc về nó?
  • Root tổng hợp cho 3 đối tượng đó ở đâu?

Nếu bạn chọn Danh từ thực thể để đại diện cho Đối tượng miền, một số điều có thể xảy ra:

  • Order, CustomerProductcó nguy cơ phát triển thành "thần" đối tượng
  • Nguy cơ kết thúc với một Managerđối tượng thần duy nhất để gắn kết mọi thứ lại với nhau.
  • Các đối tượng đó có nguy cơ trở nên gắn kết chặt chẽ với nhau - có thể khó thực hiện các yêu cầu miền mà không vượt qua this(hoặc self)
  • Nguy cơ phát triển trừu tượng "rò rỉ" - tức là các đối tượng miền được dự kiến ​​sẽ phơi bày hàng tá get/ setphương thức làm suy yếu việc đóng gói (hoặc, nếu bạn không, thì một số lập trình viên khác có thể sẽ về sau ..).
  • Nguy cơ đối tượng miền trở nên cồng kềnh với hỗn hợp dữ liệu kinh doanh phức tạp (ví dụ: dữ liệu người dùng nhập qua giao diện người dùng) và trạng thái tạm thời (ví dụ: 'lịch sử' các hành động của người dùng khi đơn hàng đã được sửa đổi).

DDD, OO Design và Plain Model

Một quan niệm sai lầm phổ biến liên quan đến DDD và OO Design là các mô hình "đơn giản" bằng cách nào đó là 'xấu' hoặc 'chống mẫu'. Martin Fowler đã viết một bài báo mô tả Mô hình miền thiếu máu - nhưng khi ông nói rõ trong bài viết, bản thân DDD không nên 'mâu thuẫn' với cách tiếp cận tách biệt giữa các lớp

"Cũng cần nhấn mạnh rằng việc đưa hành vi vào các đối tượng miền không được mâu thuẫn với cách tiếp cận vững chắc của việc sử dụng phân lớp để tách logic miền khỏi những thứ như trách nhiệm trình bày và trách nhiệm trình bày. Logic nên trong đối tượng miền là logic miền - tính hợp lệ , quy tắc kinh doanh - bất cứ điều gì bạn muốn gọi nó. "

Nói cách khác, sử dụng Mô hình đơn giản để giữ dữ liệu doanh nghiệp được chuyển giữa các lớp khác (ví dụ: mô hình Đơn hàng được truyền bởi ứng dụng người dùng khi người dùng muốn tạo đơn hàng mới) không giống với "Mô hình miền thiếu máu". Các mô hình dữ liệu 'đơn giản' thường là cách tốt nhất để theo dõi dữ liệu và truyền dữ liệu giữa các lớp (chẳng hạn như dịch vụ web REST, cửa hàng lưu trữ lâu bền, Ứng dụng hoặc Giao diện người dùng, v.v.).

Logic nghiệp vụ có thể xử lý dữ liệu trong các mô hình đó và có thể theo dõi chúng như một phần của trạng thái kinh doanh - nhưng sẽ không nhất thiết phải sở hữu các mô hình đó.

Rễ tổng hợp

Nhìn một lần nữa tại ví dụ miền Objects - SalesOrderCheckout, PendingOrdersStream, WarehouseOrderDespatcher, OrderRefundProcessorvẫn còn không rõ ràng tổng hợp gốc; nhưng điều đó không thực sự quan trọng bởi vì các Đối tượng Miền này có các trách nhiệm riêng biệt dường như không chồng chéo.

Về mặt chức năng, không cần phải SalesOrderCheckoutnói chuyện với PendingOrdersStreamvì công việc trước đây đã hoàn tất khi nó đã thêm một đơn đặt hàng mới vào Cơ sở dữ liệu; mặt khác, PendingOrdersStreamcó thể lấy các đơn đặt hàng mới từ Cơ sở dữ liệu. Các đối tượng này thực sự không cần phải tương tác trực tiếp với nhau (Có lẽ Người hòa giải sự kiện có thể cung cấp thông báo giữa hai đối tượng, nhưng tôi hy vọng mọi khớp nối giữa các đối tượng này sẽ rất lỏng lẻo)

Có lẽ Root tổng hợp sẽ là một IoC Container, đưa một hoặc nhiều đối tượng miền đó vào Bộ điều khiển giao diện người dùng, cũng cung cấp cơ sở hạ tầng khác như EventMediatorRepository. Hoặc có lẽ nó sẽ là một loại Dịch vụ Dàn nhạc nhẹ nằm trên cùng của Lớp nghiệp vụ.

Rễ tổng hợp không nhất thiết phải là Đối tượng miền. Vì mục đích giữ Tách biệt các mối quan tâm giữa các đối tượng Miền, nói chung là một điều tốt khi gốc tổng hợp là một đối tượng riêng biệt không có logic nghiệp vụ.


3
Tôi đánh giá thấp vì câu trả lời của bạn kết hợp các khái niệm từ Entity Framework, một công nghệ dành riêng cho Microsoft với Domain Driven Design, từ một cuốn sách được viết bởi Eric Evans cùng tên. Bạn có một số câu trong câu trả lời của mình mâu thuẫn trực tiếp với sách DDD và câu hỏi này không đề cập đến Entity Framework nhưng cụ thể được gắn thẻ với DDD. Cũng không có đề cập đến sự kiên trì nào trong câu hỏi vì vậy tôi không thấy các bảng cơ sở dữ liệu có liên quan ở đâu.
RibaldEddie

@RibaldEddie Cảm ơn bạn đã dành thời gian để xem lại câu trả lời và nhận xét, tôi đồng ý việc đề cập đến dữ liệu liên tục không thực sự cần phải có trong câu trả lời vì vậy tôi đã điều chỉnh lại để xóa nó. Trọng tâm chính của câu trả lời có thể được tóm tắt là "Danh từ thực thể thường không phải là tên lớp đối tượng miền rất tốt do xu hướng trở thành đối tượng thần được kết hợp chặt chẽ", hy vọng rằng thông điệp và lý do các yêu cầu / hành vi chức năng của WRT đã rõ ràng hơn ?
Ben Cottrell

Cuốn sách DDD không có khái niệm IIRC đó. Nó có các Thực thể, chỉ là các lớp mà khi được tạo ngay lập tức có một danh tính duy nhất và duy nhất để hai trường hợp tách biệt ngụ ý hai điều duy nhất và có thể tồn tại, tương phản với các Đối tượng Giá trị không có bất kỳ danh tính nào và không tồn tại theo thời gian . Trong cuốn sách, cả hai đối tượng Thực thể và Giá trị đều là đối tượng miền.
RibaldEddie

10

Các tiêu chí để xác định một tổng hợp là gì?

Chúng ta hãy quay trở lại những điều cơ bản của cuốn sách lớn màu xanh:

Uẩn: Một cụm các đối tượng liên quan được coi là một đơn vị cho mục đích thay đổi dữ liệu . Tham chiếu bên ngoài được giới hạn ở một thành viên của AGGREGATE, được chỉ định làm gốc. Một tập hợp các quy tắc nhất quán được áp dụng trong các ranh giới của AGGREGATE.

Mục tiêu là duy trì sự bất biến. Nhưng đó cũng là để quản lý danh tính địa phương đúng cách, tức là nhận dạng các đối tượng không có ý nghĩa một mình.

Ordervà chắc chắn Order linethuộc về một cụm như vậy. Ví dụ:

  • Xóa một Order, sẽ yêu cầu xóa tất cả các dòng của nó.
  • Xóa một dòng có thể yêu cầu đánh số lại các dòng sau
  • Thêm một dòng mới sẽ yêu cầu xác định nulber dòng dựa trên tất cả các dòng khác của cùng một thứ tự.
  • Thay đổi một số thông tin đặt hàng, chẳng hạn như tiền tệ, có thể ảnh hưởng đến ý nghĩa của giá trong chi tiết đơn hàng (hoặc yêu cầu tính toán lại giá).

Vì vậy, ở đây tổng hợp đầy đủ là cần thiết để đảm bảo quy tắc nhất quán và bất biến.

Khi nào thì dừng?

Bây giờ, bạn mô tả một số quy tắc kinh doanh và lập luận rằng để đảm bảo chúng, bạn cần xem khách hàng là một phần của tổng hợp:

Chúng tôi có một quy tắc kinh doanh nói rằng Khách hàng chỉ có thể có một đơn hàng chưa được gửi tại một thời điểm.

Dĩ nhiên, tại sao không. Hãy xem ý nghĩa: đơn hàng sẽ luôn được truy cập thông qua khách hàng. Đây có phải là cuộc sống thực? Khi công nhân đang điền vào các hộp để giao đơn đặt hàng, họ có cần đọc mã vạch của khách hàng và mã vạch đơn hàng để truy cập đơn hàng không? Trên thực tế, nói chung, danh tính của một Đơn hàng là toàn cầu đối với khách hàng và tính độc lập tương đối này gợi ý giữ anh ta bên ngoài tổng hợp.

Ngoài ra, các quy tắc kinh doanh này trông giống như các chính sách: đó là một quyết định tùy tiện của công ty để điều hành quy trình của họ với các quy tắc này. Nếu các quy tắc không được tôn trọng, ông chủ có thể không hài lòng, nhưng dữ liệu không thực sự không nhất quán. Và hơn nữa, qua đêm "mỗi khách hàng một đơn hàng chưa được gửi tại một thời điểm" có thể trở thành "mười đơn hàng chưa được gửi cho mỗi khách hàng" hoặc thậm chí "độc lập với khách hàng, hàng trăm đơn hàng chưa được gửi cho mỗi kho", do đó tổng hợp có thể không còn hợp lý.


1

trong miền vấn đề của chúng tôi, chúng tôi có một quy tắc kinh doanh nói rằng Khách hàng chỉ có thể có một đơn hàng chưa được gửi tại một thời điểm.

Trước khi bạn đi quá sâu vào lỗ thỏ đó, bạn nên xem lại cuộc thảo luận của Greg Young về tính nhất quán của tập hợp và đặc biệt:

Tác động kinh doanh của việc có một thất bại là gì?

Bởi vì trong rất nhiều trường hợp, câu trả lời đúng không phải là cố gắng ngăn điều sai xảy ra, mà thay vào đó là tạo ra các báo cáo ngoại lệ khi có thể có vấn đề.

Nhưng, giả sử rằng nhiều đơn đặt hàng chưa được gửi là một trách nhiệm quan trọng đối với doanh nghiệp của bạn ....

Có, nếu bạn muốn đảm bảo rằng chỉ có một đơn hàng chưa được gửi, thì cần phải có một số tổng hợp có thể xem tất cả các đơn đặt hàng cho một khách hàng.

Tổng hợp đó không nhất thiết là tổng hợp của khách hàng .

Nó có thể là một cái gì đó giống như một hàng đợi đơn hàng hoặc lịch sử đặt hàng, trong đó tất cả các đơn đặt hàng cho một khách hàng cụ thể đi vào cùng một hàng đợi. Từ những gì bạn đã nói, nó không cần tất cả dữ liệu hồ sơ của khách hàng, do đó không nên là một phần của tổng hợp này.

Nhưng khi nói đến vận chuyển, họ thực hiện tất cả các hành động của mình trên Đơn hàng chứ không phải khách hàng.

Có, khi bạn thực sự làm việc với việc hoàn thành và kéo các trang tính, chế độ xem lịch sử không đặc biệt phù hợp.

Chế độ xem lịch sử, để thực thi bất biến của bạn, chỉ cần id đơn hàng và trạng thái xử lý hiện tại của nó. Điều đó không nhất thiết phải là một phần của tổng hợp giống như thứ tự - hãy nhớ, các ranh giới tổng hợp là về việc quản lý thay đổi, không cấu trúc các khung nhìn.

Vì vậy, có thể là bạn xử lý đơn hàng dưới dạng tổng hợp và lịch sử đơn hàng dưới dạng tổng hợp riêng biệt và điều phối hoạt động giữa hai đơn hàng.


1

Bạn đã thiết lập một ví dụ người rơm. Nó quá đơn giản và tôi nghi ngờ nó phản ánh một hệ thống trong thế giới thực. Tôi sẽ không mô hình hóa các Thực thể đó và hành vi liên quan của chúng theo cách mà bạn đã chỉ định vì điều đó.

Các lớp học của bạn cần mô hình hóa trạng thái của một đơn hàng theo cách được phản ánh trong nhiều tập hợp. Ví dụ khi puts khách hàng hệ thống vào trạng thái nơi yêu cầu đặt hàng của khách hàng cần phải được xử lý, tôi có thể tạo ra một tổng hợp miền đối tượng thực thể gọi là CustomerOrderRequesthay PendingCustomerOrderhoặc thậm chí chỉ CustomerOrder, hoặc bất cứ ngôn ngữ những ứng dụng kinh doanh, và nó có thể giữ một con trỏ đến cả Khách hàng và OrderLines và sau đó có một phương thức canCustomerCompleteOrder()được gọi từ lớp dịch vụ.

Đối tượng miền này sẽ chứa logic nghiệp vụ để xác định xem đơn hàng có hợp lệ hay không.

Nếu đơn hàng hợp lệ và được xử lý thì tôi có một số cách để chuyển đối tượng này sang đối tượng khác đại diện cho đơn hàng đã xử lý.

Tôi nghĩ rằng vấn đề với sự hiểu biết của bạn là bạn đang sử dụng một ví dụ đơn giản hóa quá mức của các tổng hợp. A PendingOrdercó thể là tổng hợp riêng của nó tách biệt với một UndeliveredOrderhoặc một lần nữa tách biệt với một DeliveredOrderhoặc một CancelledOrderhoặc bất cứ điều gì.


Mặc dù nỗ lực của bạn về ngôn ngữ trung lập về giới tính rất thú vị, tôi sẽ lưu ý rằng phụ nữ không bao giờ đứng trên cánh đồng để dọa lũ quạ.
Robert Harvey

@RobertHarvey đó là một điều kỳ lạ để tập trung vào bài viết của tôi. Bù nhìn và hình nộm đều thường xuyên xuất hiện ở dạng nữ trong suốt lịch sử.
RibaldEddie

Bạn sẽ không tạo ra sự khác biệt trong bài viết của mình nếu bạn không coi nó là quan trọng. Là một vấn đề của ngôn ngữ học, thuật ngữ này là "người rơm;" bất kỳ sự dè dặt nào về vấn đề tình dục gần như chắc chắn bị ảnh hưởng bởi yếu tố "anh ta đang nói về cái quái gì" được tạo ra bằng cách phát minh ra thuật ngữ của riêng bạn.
Robert Harvey

5
@RobertHarvey nếu ai đó biết người rơm nghĩa là gì, tôi chắc chắn họ có thể hiểu người rơm nghĩa là gì nếu họ không nghe thấy thuật ngữ đó. Chúng ta có thể tập trung vào chất của bài viết của tôi xin vui lòng viết phần mềm không?
RibaldEddie

1

Vaughn Vernon đã đề cập đến điều này trong cuốn sách "Thực hiện thiết kế hướng tên miền" ở đầu Chương 7 (Dịch vụ):

"Thường thì dấu hiệu tốt nhất cho thấy bạn nên tạo Dịch vụ trong mô hình miền là khi thao tác bạn cần thực hiện cảm thấy không phù hợp như một phương thức trên Tổng hợp hoặc Đối tượng Giá trị".

Vì vậy, trong trường hợp này, có thể có một dịch vụ tên miền gọi là "CreatOrderService" lấy một thể hiện của Khách hàng và danh sách các mục cho đơn đặt hàng.

class CreateOrderService
{
    public Order CreateOrder(Customer customer, ThingsInTheOrder thingsInTheOrder)
    {
        // Get all the orders for the customer
        // Check if any of the things to be ordered exist in previous orders   
        // If none have been previously ordered, create the order and return it
        // Otherwise return null 
    }
}

1
Bạn có thể vui lòng giải thích thêm về cách dịch vụ tên miền có thể giúp giải quyết mối quan tâm đồng thời trong câu hỏi không?
ivenxu
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.