Sử dụng thành phần và kế thừa cho DTOs


13

Chúng tôi có API Web ASP.NET cung cấp API REST cho Ứng dụng Trang đơn của chúng tôi. Chúng tôi sử dụng DTO / POCO để truyền dữ liệu thông qua API này.

Vấn đề là bây giờ, rằng các DTO này đang ngày càng lớn hơn theo thời gian, vì vậy bây giờ chúng tôi muốn cấu trúc lại các DTO.

Tôi đang tìm kiếm "cách thực hành tốt nhất" về cách thiết kế DTO: Hiện tại chúng tôi có các DTO nhỏ chỉ bao gồm các trường loại giá trị, ví dụ:

public class UserDto
{
    public int Id { get; set; }

    public string Name { get; set; }
}

Các DTO khác sử dụng UserDto này theo thành phần, ví dụ:

public class TaskDto
{
    public int Id { get; set; }

    public UserDto AssignedTo { get; set; }
}

Ngoài ra, có một số DTO mở rộng được xác định bằng cách kế thừa từ những người khác, ví dụ:

public class TaskDetailDto : TaskDto
{
    // some more fields
}

Vì một số DTO đã được sử dụng cho một số điểm cuối / phương thức (ví dụ GET và PUT), chúng đã được mở rộng dần dần bởi một số trường theo thời gian. Và do sự kế thừa và thành phần, các DTO khác cũng trở nên lớn hơn.

Câu hỏi của tôi bây giờ là thừa kế và thành phần không thực hành tốt? Nhưng khi chúng ta không sử dụng lại chúng, có cảm giác như viết cùng một mã nhiều lần. Có phải là một thực tế xấu khi sử dụng DTO cho nhiều điểm cuối / phương thức, hoặc nên có các DTO khác nhau, chỉ khác nhau ở một số sắc thái?


6
Tôi không thể nói cho bạn biết bạn nên làm gì, nhưng theo kinh nghiệm của tôi khi làm việc với DTO, kế thừa và sáng tác sớm hơn sau đó săn lùng bạn như một ly cà phê tồi. Tôi sẽ không bao giờ sử dụng lại DTO nữa. Không bao giờ. Thêm vào đó, tôi không coi các DTO tương tự là vi phạm DRY. Hai điểm cuối trả về cùng một đại diện có thể sử dụng lại các DTO rất giống nhau. Hai điểm cuối trả về các biểu diễn tương tự không trả về cùng một DTO, vì vậy tôi tạo các DTO cụ thể cho từng điểm cuối . Nếu tôi bị buộc phải chọn, bố cục sẽ ít gặp vấn đề hơn về lâu dài.
Laiv

@Laiv đây là câu trả lời chính xác cho câu hỏi, đừng. Không chắc chắn lý do tại sao bạn đặt nó làm nhận xét
TheCatWhisperer 17/07/17

2
@Laiv: Bạn dùng gì để thay thế? Theo kinh nghiệm của tôi, những người đấu tranh với điều này chỉ đơn giản là lật đổ nó. DTO chỉ là một thùng chứa dữ liệu và đó là tất cả.
Robert Harvey

TheCatWhisperer vì lập luận của tôi chủ yếu dựa trên quan điểm. Tôi vẫn đang cố gắng giải quyết loại vấn đề này trong các dự án của mình. @RobertHarvey đúng, không biết tại sao tôi có xu hướng nhìn mọi thứ khó hơn thực tế. Tôi vẫn đang nghiên cứu giải pháp. Tôi đã hoàn toàn tin rằng HAL là mô hình có nghĩa là để giải quyết những vấn đề này, nhưng đọc câu trả lời của bạn, tôi nhận ra rằng tôi cũng thực hiện DTO của mình. Vì vậy, tôi sẽ đưa vào thực tế phương pháp của bạn đầu tiên. Những thay đổi sẽ ít kịch tính hơn là chuyển hoàn toàn sang HATEOAS.
Laiv

Hãy thử điều này để có một cuộc thảo luận tốt có thể giúp bạn: stackoverflow.com/questions/6297322/
Khăn

Câu trả lời:


9

Như một cách thực hành tốt nhất, hãy thử và làm cho các DTO của bạn ngắn gọn nhất có thể. Chỉ trả lại những gì bạn cần trả lại. Chỉ sử dụng những gì bạn cần sử dụng. Nếu điều đó có nghĩa là một vài DTO thêm, vì vậy hãy là nó.

Trong ví dụ của bạn, một tác vụ chứa một người dùng. Một người có lẽ không cần một đối tượng người dùng đầy đủ ở đó, có thể chỉ là tên của người dùng được giao nhiệm vụ. Chúng tôi không cần phần còn lại của tài sản người dùng.

Giả sử bạn muốn gán lại một nhiệm vụ cho một người dùng khác, người ta có thể muốn chuyển một đối tượng người dùng đầy đủ và một đối tượng nhiệm vụ đầy đủ trong bài đăng lại. Nhưng, những gì thực sự cần thiết chỉ là Id nhiệm vụ và Id người dùng. Đó là hai thông tin duy nhất cần thiết để phân công lại một nhiệm vụ, vì vậy hãy mô hình hóa DTO như vậy. Điều này thường có nghĩa là có một DTO riêng cho mỗi cuộc gọi còn lại nếu bạn phấn đấu cho một mô hình DTO nạc.

Ngoài ra, đôi khi người ta có thể cần sự kế thừa / thành phần. Hãy nói rằng một người có một công việc. Một công việc có nhiều nhiệm vụ. Trong trường hợp đó, nhận được một công việc cũng có thể trả về danh sách các nhiệm vụ cho công việc. Vì vậy, không có quy tắc chống lại thành phần. Nó phụ thuộc vào những gì đang được mô hình hóa.


Càng ngắn gọn càng tốt cũng có nghĩa là tôi nên tạo lại DTO nếu chúng chỉ khác nhau ở một số chi tiết - phải không?
sĩ quan

1
@officer - Nói chung là có.
Jon Raynor

2

Trừ khi hệ thống của bạn hoàn toàn dựa trên các hoạt động CRUD, DTO của bạn quá chi tiết. Hãy thử tạo các điểm cuối thể hiện các quy trình kinh doanh hoặc tạo tác. Cách tiếp cận này ánh xạ độc đáo đến một lớp logic kinh doanh và "thiết kế hướng tên miền" của Eric Evans.

Ví dụ: giả sử bạn có điểm cuối trả về dữ liệu cho hóa đơn để có thể hiển thị trên màn hình hoặc biểu mẫu cho người dùng cuối. Trong mô hình CRUD, bạn sẽ cần một số cuộc gọi đến các điểm cuối của mình để tập hợp các thông tin cần thiết: tên, địa chỉ thanh toán, địa chỉ giao hàng, chi tiết đơn hàng. Trong ngữ cảnh giao dịch kinh doanh, một DTO từ một điểm cuối duy nhất có thể trả về tất cả thông tin này cùng một lúc.


Robert, ý của bạn là một gốc tổng hợp ở đây?
johnny

Hiện tại các DTO của chúng tôi được thiết kế để cung cấp toàn bộ dữ liệu cho một biểu mẫu, ví dụ: khi hiển thị danh sách việc cần làm, TodoListDTO có Danh sách các Nhiệm vụ. Nhưng vì chúng tôi đang sử dụng lại TaskDTO, mỗi người trong số họ cũng có một UserDTO. Vì vậy, vấn đề là số lượng lớn dữ liệu được truy vấn trong phần phụ trợ và được gửi qua dây.
sĩ quan

Là tất cả các dữ liệu cần thiết?
Robert Harvey

1

Thật khó để thiết lập các thực tiễn tốt nhất cho một cái gì đó "linh hoạt" hoặc trừu tượng như một DTO. Về cơ bản, DTO chỉ là đối tượng để truyền dữ liệu nhưng tùy thuộc vào điểm đến hoặc lý do chuyển, bạn có thể muốn áp dụng các "thực tiễn tốt nhất" khác nhau.

Tôi khuyên bạn nên đọc Mô hình kiến ​​trúc ứng dụng doanh nghiệp của Martin Fowler . Có cả một chương dành riêng cho các mẫu, trong đó các DTO có được một phần thực sự chi tiết.

Ban đầu, chúng được "thiết kế" để sử dụng trong các cuộc gọi từ xa đắt tiền, nơi bạn có thể sẽ cần rất nhiều dữ liệu từ các phần khác nhau trong logic của bạn; DTO sẽ thực hiện chuyển dữ liệu trong một cuộc gọi.

Theo tác giả, DTOs không có ý định sử dụng trong môi trường địa phương, nhưng một số người đã tìm thấy cách sử dụng chúng. Thông thường chúng được sử dụng để thu thập thông tin từ các POCO khác nhau thành một thực thể duy nhất cho GUI, API hoặc các lớp khác nhau.

Bây giờ, với tính kế thừa, việc sử dụng lại mã giống như một tác dụng phụ của kế thừa hơn là mục tiêu chính của nó; thành phần, mặt khác, được thực hiện với việc sử dụng lại mã làm mục tiêu chính.

Một số người khuyên nên sử dụng thành phần và kế thừa cùng nhau, sử dụng điểm mạnh của cả hai và cố gắng giảm thiểu điểm yếu của họ. Sau đây là một phần của quy trình tinh thần của tôi khi chọn hoặc tạo DTO mới hoặc bất kỳ lớp / đối tượng mới nào cho vấn đề đó:

  • Tôi sử dụng sự kế thừa với các DTO bên trong cùng một lớp hoặc cùng một bối cảnh. Một DTO sẽ không bao giờ thừa kế từ POCO, BLL DTO sẽ không bao giờ thừa kế từ DAL DTO, v.v.
  • Nếu tôi thấy mình đang cố gắng giấu một trường khỏi DTO, tôi sẽ cấu trúc lại và có thể sử dụng bố cục thay thế.
  • Nếu rất ít trường khác nhau từ một DTO cơ sở là tất cả những gì tôi cần, tôi sẽ đặt chúng vào một DTO phổ quát. DTO phổ quát chỉ được sử dụng trong nội bộ.
  • Một POCO / DTO cơ sở gần như sẽ không bao giờ được sử dụng cho bất kỳ logic nào, theo cách đó, cơ sở chỉ trả lời cho các nhu cầu của con cái nó. Nếu tôi cần sử dụng cơ sở, tôi sẽ tránh thêm bất kỳ lĩnh vực mới nào mà con của nó sẽ không bao giờ sử dụng.

Một số trong số chúng có thể không phải là thực tiễn "tốt nhất", chúng hoạt động khá tốt cho các dự án tôi đang thực hiện nhưng bạn cần nhớ rằng không có kích thước nào phù hợp với tất cả. Trong trường hợp DTO phổ quát, bạn nên cẩn thận, chữ ký phương thức của tôi trông như thế này:

public void DoSomething(BaseDTO base) {
    //Some code 
}

Nếu bất kỳ phương thức nào cần DTO riêng, tôi sẽ thực hiện kế thừa và thường thì thay đổi duy nhất tôi cần thực hiện là tham số, mặc dù đôi khi tôi cần đào sâu hơn cho các trường hợp cụ thể.

Từ ý kiến ​​của bạn tôi nhận thấy rằng bạn đang sử dụng các DTO lồng nhau. Nếu các DTO lồng nhau của bạn chỉ bao gồm một danh sách các DTO khác, tôi nghĩ điều tốt nhất để làm là mở danh sách.

Tùy thuộc vào lượng dữ liệu bạn cần hiển thị hoặc để làm việc, có thể là một ý tưởng tốt để tạo các DTO mới giới hạn dữ liệu; ví dụ: nếu UserDTO của bạn có nhiều trường và bạn chỉ cần 1 hoặc 2, thì tốt hơn là có DTO chỉ với các trường đó. Xác định lớp, bối cảnh, cách sử dụng và tiện ích của DTO sẽ giúp ích rất nhiều khi thiết kế nó.


Tôi không thể thêm nhiều hơn 2 liên kết trong câu trả lời của mình, đây là một số thông tin thêm về các DTO địa phương. Tôi biết rằng thông tin rất cũ nhưng tôi nghĩ một số thông tin có thể vẫn còn liên quan.
IvanGrasp

1

Sử dụng thành phần trong DTOs là một thực hành hoàn toàn tốt.

Kế thừa giữa các loại DTO cụ thể là một thực tiễn xấu.

Đối với một điều, trong một ngôn ngữ như C #, một thuộc tính được triển khai tự động có rất ít chi phí bảo trì nên việc sao chép chúng (tôi thực sự ghê tởm trùng lặp) không gây hại như thường lệ.

Một lý do để không sử dụng sự kế thừa cụ thể giữa các DTO là một số công cụ nhất định sẽ vui vẻ ánh xạ chúng thành các loại sai.

Ví dụ: nếu bạn sử dụng tiện ích cơ sở dữ liệu như Dapper (tôi không khuyên dùng nhưng nó phổ biến) thực hiện class name -> table namesuy luận, bạn có thể dễ dàng lưu loại dẫn xuất dưới dạng loại cơ sở cụ thể ở đâu đó trong hệ thống phân cấp và do đó mất dữ liệu hoặc tệ hơn .

Một lý do sâu xa hơn để không sử dụng tính kế thừa giữa các DTO là nó không nên được sử dụng để chia sẻ việc triển khai giữa các loại không có mối quan hệ "rõ ràng". Đối với tôi, TaskDetailkhông giống như một kiểu con Task. Nó có thể dễ dàng trở thành một tài sản của một Taskhoặc, tệ hơn, nó có thể là một siêu kiểu của Task.

Bây giờ, một điều bạn có thể quan tâm là duy trì tính nhất quán giữa tênloại thuộc tính của các DTO khác nhau có liên quan.

Do đó, việc kế thừa các loại cụ thể sẽ giúp đảm bảo tính nhất quán như vậy, tốt hơn là sử dụng các giao diện (hoặc các lớp cơ sở ảo thuần túy trong C ++) để duy trì tính nhất quán đó.

Hãy xem xét các DTO sau đây

interface IIdentity
{
    int Id { get; set; }
}

interface INamed
{
    string Name { get; set; }
}

public class UserDto: IIdentity, INamed
{
    public int Id { get; set; }

    public string Name { get; set; }

    // User specific properties
}

public class TaskDto: IIdentity
{
    public int Id { get; set; }

    // Task specific properties
}
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.