CodeGym /Các khóa học /C# SELF /Vấn đề với các class dùng để truyền dữ liệu (DTO)

Vấn đề với các class dùng để truyền dữ liệu (DTO)

C# SELF
Mức độ , Bài học
Có sẵn

1. Làm quen với DTO

DTO (Data Transfer Object, "đối tượng truyền dữ liệu") là một thuật ngữ xuất phát từ lập trình chuyên nghiệp, đặc biệt là trong thế giới ứng dụng phân tán và các service (ví dụ như khi một service gọi service khác qua mạng). Thực chất, nó chỉ là một object — đa phần là class C# bình thường — với nhiệm vụ duy nhất: chứa một tập dữ liệu và truyền an toàn giữa các tầng của chương trình hoặc giữa các ứng dụng.

Hãy tưởng tượng một cuộc trò chuyện giữa client và server. Mỗi lần user gửi tin nhắn, client sẽ tạo một object chứa text, thời gian và tên user, gửi cho server, còn server sẽ quyết định xử lý thế nào. Sẽ tuyệt hơn nếu object này không có gì thừa: chỉ những gì cần thiết để truyền dữ liệu. Đó chính là class DTO.

public class UserDto
{
    public int Id { get; set; }
    public string Name { get; set; }
}
DTO kinh điển: chỉ có property, không có logic gì cả

Xong! Không có business logic, không có method phức tạp — chỉ có property thôi.

Class chỉ để truyền dữ liệu

Hãy tưởng tượng bạn làm dịch vụ giao pizza. Người giao hàng của bạn (DTO) không cần biết nấu pizza, nhận order hay sửa xe đạp. Việc duy nhất của họ là mang hộp pizza từ điểm A đến điểm B. Class DTO cũng vậy: nhiệm vụ là lưu trữ dữ liệu đơn giản. Không cần tự kiểm tra tính hợp lệ, không cần biết cách hiển thị trên UI — chỉ lưu trữ thôi.

  • DTO — kiểu như "người không nghề" hay "object không logic", chỉ mang dữ liệu đi thôi.
  • Dùng để không làm bẩn business logic với chi tiết truyền dữ liệu: vali càng nhỏ càng dễ mang.

2. Vấn đề của các class DTO thông thường trong thực tế

Nghe thì có vẻ ngon: khai báo class với auto-property, sống khỏe! Nhưng thực tế không đơn giản vậy đâu.

Tính có thể thay đổi: chuyện gì có thể xảy ra?

public class ProductDto
{
    public int Id { get; set; }
    public string Name { get; set; }
}
DTO với set public — rất dễ bị thay đổi dữ liệu ngoài ý muốn

Giờ tưởng tượng ở đâu đó trong code, ai đó vô tình đổi property:

ProductDto dto = new ProductDto { Id = 1, Name = "Sữa" };
SomeApiProcess(dto);
dto.Name = "Sữa chua"; // Ôi trời!

Vì tất cả property đều có set public, rất dễ vô tình làm hỏng object hoặc tệ hơn, đổi ở một chỗ mà nó lại "lan" ra cả hệ thống. Điều này thành vấn đề lớn khi object được nhiều module và thread cùng dùng.

Copy và so sánh — công việc nhàm chán không hồi kết

Giả sử bạn muốn tạo bản copy của ProductDto, chỉ đổi mỗi tên.

var otherDto = new ProductDto { Id = dto.Id, Name = "Phô mai" };

Nếu property nhiều, việc này thành một quá trình copy thủ công vừa chán vừa dễ bug.

Còn nếu bạn cần so sánh object? Lại phải override EqualsGetHashCode, mà thường thì làm qua loa hoặc quên luôn.

Bất biến — không dành cho class "đơn giản"

Hầu hết trường hợp DTO nên là bất biến: nhận dữ liệu một lần — không đổi nữa. Nhưng class với auto-property thì không hợp: ai cũng có thể đổi bất kỳ field nào.

3. Thực tế trong dự án lớn sẽ ra sao?

Cùng xem một ví dụ thực tế nhé.

Bạn là dev ở một trang thương mại điện tử khổng lồ. Bạn cần truyền thông tin đơn hàng qua mạng. Bạn tạo class như này:

public class OrderDto
{
    public int Id { get; set; }
    public string Customer { get; set; }
    public double Total { get; set; }
}

Mọi thứ ổn cho đến khi có vài trăm ngàn đơn mỗi tuần. Đống microservice bắt đầu chuyền DTO qua lại, ai đó quên mất Total phải tính chứ không phải nhập tay, ai đó vô tình đổi Customer sau khi gửi... Và thế là bug xuất hiện, rất khó bắt.

Làm sao cải thiện?

  • Dùng chỉ get-property (không có set).
  • Tạo constructor đặc biệt để set value chỉ khi tạo object.
  • Override EqualsGetHashCode

Bạn thấy vấn đề chưa? Thứ lẽ ra chỉ là "vali" đơn giản, bỗng hóa thành quái vật rắc rối.

4. Tóm tắt các vấn đề điển hình của class DTO

Dự án nhỏ thì bug do đổi DTO còn bắt được bằng tay. Dự án lớn — thảm họa luôn. DTO dùng cho xác thực, chuyển tiền, xử lý đơn hàng, — bất kỳ thay đổi dữ liệu trái phép nào cũng có thể thành mất tiền hoặc (chết thật!) dính án hình sự.

Dưới đây là "cheat sheet" các vấn đề mà gần như ai code DTO bằng class cũng gặp:

Vấn đề Mô tả
Tính có thể thay đổi Dữ liệu dễ bị đổi ở bất cứ đâu — dễ sinh bug (đặc biệt khi đa luồng)
Clone Copy object rất bất tiện, thường phải làm tay
So sánh Mặc định so sánh theo reference, không phải theo value
Hỗ trợ with-copy Muốn copy và đổi một phần dữ liệu mà không phải copy tay
Lồng nhau không rõ ràng DTO lồng nhau phải copy hết, rất phiền

5. Ví dụ: truyền dữ liệu giữa các tầng trong app của chúng ta

Giả sử bạn có app ghi chú, lưu task. Trước đây bạn sẽ có class như này:

public class TaskDto
{
    public int Id { get; set; }
    public string Description { get; set; }
    public DateTime DueDate { get; set; }
}

Bạn đọc TaskDto từ file, thêm vào collection, rồi hiển thị lên màn hình. Mọi thứ ổn... cho đến khi ai đó ở module khác vô tình đổi DueDate ngay trước khi serialize — và user bị rối tung.

7. Có cách nào ngon hơn không?

Có chứ! Và không chỉ có mà còn nên dùng! Để giải quyết các vấn đề này, C# có một cấu trúc đặc biệt — record. Nó giải quyết gần hết nỗi đau với class DTO, gần như là "phép thuật".

record làm được gì:

  • Mặc định so sánh theo value: so sánh object theo nội dung, không phải reference.
  • Bất biến: chỉ có get-property, value set qua constructor.
  • Clone dễ dàng: có cú pháp with để copy và đổi một phần.
  • Tự động sinh method so sánh và in ra.
public record TaskDto(int Id, string Description, DateTime DueDate);
DTO dựa trên record: gọn, an toàn, tiện lợi!

Ngon chưa? Quá ngon! Nhưng phần này — để dành cho bài sau nhé.

2
Nhiệm vụ
C# SELF, mức độ, bài học
Đã khóa
Tạo một class DTO đơn giản
Tạo một class DTO đơn giản
2
Nhiệm vụ
C# SELF, mức độ, bài học
Đã khóa
Sử dụng DTO để truyền dữ liệu giữa các phương thức
Sử dụng DTO để truyền dữ liệu giữa các phương thức
Bình luận
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION