[DDD Thực chiến] Entity vs Value Object — Sự khác biệt sống còn mà Junior hay nhầm nhất
Trong suốt 15 năm làm phần mềm và review code, lỗi kiến trúc phổ biến nhất mà tôi thấy các bạn Junior (thậm chí là Middle) mắc phải khi tiếp cận Domain-Driven Design (DDD) là không phân biệt được Entity và Value Object (VO).
Hậu quả? Các bạn tạo ra một hệ thống "Anemic Domain Model" (Mô hình domain thiếu máu). Mọi thứ đều được thiết kế từ Database đi lên, class nào cũng bị nhét một cái Id làm Primary Key, và các logic nghiệp vụ bị phân mảnh rải rác khắp nơi.
Bài viết này sẽ đả thông tư tưởng cho bạn về ranh giới giữa Entity và Value Object, kèm theo "bài test" thực chiến để áp dụng ngay vào dự án.
1. Bản chất vấn đề: Định danh (Identity) vs Giá trị (Attributes)
Sai lầm cốt lõi là chúng ta thường nhìn đối tượng dưới góc độ "Lưu dữ liệu" thay vì "Vòng đời nghiệp vụ".
Entity (Thực thể): Định nghĩa bằng một Định danh duy nhất (ID). Dù mọi thuộc tính bên trong của nó thay đổi theo thời gian, nó vẫn là chính nó.
Value Object (Đối tượng giá trị): Định nghĩa hoàn toàn bằng Giá trị của các thuộc tính. Nó không có ID. Nếu giá trị thay đổi, nó là một đối tượng hoàn toàn khác.
Để dễ hiểu, hãy dùng "Bài test tráo đổi" (Interchangeability Test).
Khi bạn thiết kế một class, hãy hỏi Business Analyst (BA): "Nếu em lấy Object A, tráo đổi hoàn toàn với một Object B có dữ liệu bề ngoài giống hệt nhau, thì hệ thống có bị lỗi logic hay mất dấu vết không?"
Nếu CÓ: Nó là Entity.
Nếu KHÔNG: Nó là Value Object.
Ví dụ thực tế đời thường: Chiếc ví và Tờ tiền
Tờ tiền (Value Object): Bạn cho bạn mình mượn tờ 100.000đ. Vài hôm sau, họ trả lại bạn một tờ 100.000đ khác. Bạn cất vào ví bình thường. Không ai quan tâm số seri (trừ khi bạn là công an), chỉ quan tâm giá trị của nó. Bạn không "chỉnh sửa" tờ 100k thành tờ 200k, bạn thay thế nó bằng tờ 200k. VO mang tính Bất biến (Immutable).
Chiếc ví (Entity): Bạn cho mượn chiếc ví. Người bạn làm mất và mua đền một chiếc ví mới, giống y hệt 100% từ chất liệu đến màu sắc. Bạn vẫn nhận ra đó không phải chiếc ví cũ của mình. Chiếc ví có tính độc nhất (Identity).
2. So sánh trong Code: Đơn hàng (Order) vs Tiền (Money)
Trong phần mềm, điều này thể hiện cực kỳ rõ qua cách chúng ta viết code và so sánh các object.
Với Value Object (Money): Chúng ta so sánh bằng giá trị. Trong hệ sinh thái .NET hiện đại, cách thanh lịch nhất để implement Value Object là sử dụng record type (từ C# 9). record mặc định hỗ trợ value equality (so sánh bằng giá trị).
C#
// Value Object: Không có ID, Bất biến (Immutable)
public record Money(decimal Amount, string Currency);
var price1 = new Money(100000, "VND");
var price2 = new Money(100000, "VND");
// Kết quả là TRUE. Hệ thống coi chúng là một vì giá trị giống hệt nhau.
Console.WriteLine(price1 == price2);
Với Entity (Order): Hai đơn hàng có thể cùng mua 1 sản phẩm, cùng giá trị 100.000đ, cùng người mua, nhưng chúng là 2 giao dịch hoàn toàn độc lập. Entity bắt buộc phải có ID và được so sánh qua ID.
C#
// Entity: Có ID, Trạng thái có thể thay đổi (Mutable)
public class Order
{
public Guid Id { get; private set; }
public Money TotalPrice { get; private set; }
public string Status { get; private set; }
public Order(Guid id, Money totalPrice)
{
Id = id;
TotalPrice = totalPrice;
Status = "Pending";
}
}
var order1 = new Order(Guid.Parse("0000-0001"), new Money(100000, "VND"));
var order2 = new Order(Guid.Parse("0000-0002"), new Money(100000, "VND"));
// Kết quả là FALSE. Dù giá trị đơn hàng giống nhau, đây là 2 thực thể khác nhau.
Console.WriteLine(order1.Id == order2.Id);
3. Ngữ cảnh là Vua (Bounded Context)
Điều quan trọng nhất mà một kỹ sư phần mềm cần nhớ: Không có class nào sinh ra đã mặc định là Entity hay Value Object. Tất cả phụ thuộc vào Ngữ cảnh nghiệp vụ (Bounded Context).
Hãy lấy ví dụ về Chỗ ngồi (Seat):
Ngữ cảnh quán Cafe/KFC: Khách bước vào, nhân viên chỉ cần quan tâm "Còn bàn 4 chỗ không?". Khách ngồi ghế nào cũng được, cái ghế hỏng thì ném đi mua cái mới thế vào. Hệ thống chỉ quản lý sức chứa. Lúc này, Chỗ ngồi là Value Object.
Ngữ cảnh rạp chiếu phim CGV: Khách mua vé chọn đích danh ghế "H12 - VIP". Ghế H12 có trạng thái (Đang trống, Đã bán, Bảo trì). Bạn không thể lấy vé ghế G12 bù cho khách được. Lúc này, Chỗ ngồi là một Entity độc lập.
4. Tổng kết: Rules of Thumb cho anh em Dev
Khi thiết kế Domain Model trong dự án tại tedux hoặc bất kỳ đâu, hãy tuân thủ các quy tắc sau:
Mặc định bắt đầu với Value Object: Hãy cố gắng thiết kế mọi thứ là Value Object nếu có thể. VO không có ID, không có trạng thái thay đổi, cực kỳ an toàn cho môi trường đa luồng (multi-thread) và rất dễ viết Unit Test.
Chỉ nâng cấp lên Entity khi bắt buộc: Nếu hệ thống có nhu cầu phải theo dõi sự thay đổi trạng thái của object đó qua thời gian (ví dụ: User, Order, Product), lúc đó mới cấp cho nó một cái ID và biến nó thành Entity.
Entity chứa VO: Thông thường, một Entity sẽ là một lớp vỏ chứa rất nhiều Value Object bên trong (Ví dụ:
UserchứaAddress,FullName,Email).
Việc tách bạch rõ ràng hai khái niệm này là bước đệm đầu tiên và quan trọng nhất để làm chủ Clean Architecture và DDD. Đừng để hệ thống của bạn bị nghẹt thở bởi hàng ngàn cái ID vô nghĩa!
Bài viết liên quan
Sử dụng kiểu tập hợp (Enum)
Enum (viết tắt của Enumeration) trong C# là một kiểu dữ liệu đặc biệt cho phép bạn định nghĩa một tập hợp các hằng số có tên
Đọc thêmTính đóng gói (Encapsulation) và best practices trong OOP
(Tính đóng gói) là một trong những nguyên tắc cơ bản của lập trình hướng đối tượng (OOP).
Đọc thêmTính trừu tượng - Abstract classes and interfaces
Tính trừu tượng (Abstraction) trong OOP là kỹ thuật ẩn đi các chi tiết triển khai và chỉ hiển thị cho người dùng những chức năng cần thiết.
Đọc thêmTính chất kế thừa (Inheritance) và đa hình (polymorphism)
Kế thừa là cơ chế cho phép một lớp (class) kế thừa các thuộc tính và phương thức từ một lớp khác.
Đọc thêmCách debug ứng dụng C#
Hướng dẫn cách debug chương trình C# trong Visual Studio và Visual Studio Code
Đọc thêmTìm hiểu về các loại Collection trong C#
Trong C#, collections là các cấu trúc dữ liệu được sử dụng để lưu trữ và quản lý các nhóm đối tượng. C# cung cấp nhiều loại collections khác nhau để phù hợp với các yêu cầu cụ thể của lập trình viên
Đọc thêmTổng quan về Generic và Non-Generic Collection
Hiểu khái niệm Generic và Non-Generic Collection và phân biệt giữa Generic Collection và Non-Generic Collection.
Đọc thêmSử dụng mảng (Arrays)
Mảng trong C# là một cấu trúc dữ liệu lưu trữ một dãy các phần tử có bộ nhớ nằm liên tiếp nhau và có kích thước cố đinh.
Đọc thêmLập trình hướng đối tượng
Lập trình hướng đối tượng (Object Oriented Programing) hay còn gọi là OOP. Là một kỹ thuật lập trình cho phép các lập trình viên có thể ánh xạ các thực thể bên ngoài đời thực và trừu tượng hoá thành các class và object trong mã nguồn.
Đọc thêmVòng lặp (loop)
Trong thực tế khi bạn cần thực thi một khối lệnh nhiều lần. Vòng lặp cho phép chúng ta thực thi một câu lệnh hoặc một khối lệnh nhiều lần.
Đọc thêm