Nguyên tắc Tell Don't Ask/Information Expert khuyến khích việc bảo vệ dữ liệu và hành vi trong cùng một lớp, giảm sự phụ thuộc giữa các lớp.
Việc áp dụng nguyên tắc này giúp mã nguồn dễ bảo trì hơn, giảm lặp lại logic và tăng tính đóng gói.
Có thể có rủi ro nếu lớp xử lý quá nhiều trách nhiệm, dẫn đến sự phụ thuộc cao, cần tách biệt mối quan tâm khi cần thiết.
Trong bài viết này, chúng ta cùng đi sâu hơn cùng một số ví dụ cụt hể để hiểu Tell Don't Ask/Information Expert (TdA/IE) là gì và áp dụng như thế nào nhé.
Nguyên tắc Tell Don't Ask (TdA) và Information Expert (IE) là hai khía cạnh của cùng một nguyên tắc thiết kế hướng đối tượng, được mô tả chi tiết trong tài liệu như Principles Wiki. Theo đó, TdA nhấn mạnh việc "nói" với đối tượng để thực hiện hành động, thay vì "hỏi" trạng thái và đưa ra quyết định bên ngoài, trong khi IE tập trung vào việc giao trách nhiệm cho lớp có thông tin liên quan nhất. Hai nguyên tắc này bổ trợ nhau, và vi phạm một trong hai thường dẫn đến vi phạm cả hai.
Nguyên tắc này xuất phát từ công trình của Craig Larman trong sách Applying UML and Patterns – An Introduction to Object-Oriented Analysis and Design and Iterative Development. Nó được chấp nhận rộng rãi trong cộng đồng phát triển phần mềm, đặc biệt trong thiết kế hướng đối tượng, vì giúp giảm sự phụ thuộc (low coupling), tăng tính kết dính (high cohesion), và tuân thủ nguyên tắc ẩn thông tin - tôi sẽ trình bày thêm những nguyên tắc này trong bài viết sắp tới.
Việc vi phạm TdA/IE thường dẫn đến các vấn đề như:
Ngược lại, áp dụng TdA/IE mang lại lợi ích như:
Để dễ hiểu hơn chúng ta đi vào phân tích một vài bài toán cụ thể nhé.
Một lớp Person để quyết định xem một người có thể uống rượu dựa trên tuổi (giả sử tuổi uống rượu là 18)
Ask version:
public class Person
{
public int Age { get; set; }
public string Name { get; set; }
public void DrinkWine(int numberOfGlasses)
{
Console.WriteLine($"{Name} drinks {numberOfGlasses} glasses of wine at age {Age}");
}
}
class Program
{
static void Main()
{
var mike = new Person { Name = "Mike", Age = 20 };
if (mike.Age >= 18)
mike.DrinkWine(2); // Kiểm tra bên ngoài, vi phạm TdA
else
Console.WriteLine($"{mike.Name} is too young to drink wine.");
}
}
Ở đây, class Program hỏi tuổi của Mike và quyết định có gọi DrinkWine hay không. Đoạn code nhìn chung không sai về business logic.
Thử tưởng tượng tiếp hỏi tuổi 100 bạn trong khu phố Mike và kiểm tra xem các bạn có được phép uống rượu hay không. Điều này tạo ra sự phụ thuộc: nếu tuổi uống rượu thay đổi (ví dụ, thành 16), chúng ta phải cập nhật tất cả các kiểm tra tương tự, làm tăng rủi ro bảo trì.
=> Tell version
public class Person
{
public const int DrinkingAge = 18;
public int Age { get; set; }
public string Name { get; set; }
public void DrinkWine(int numberOfGlasses)
{
if (Age >= DrinkingAge)
Console.WriteLine($"{Name} drinks {numberOfGlasses} glasses of wine at age {Age}");
else
throw new InvalidOperationException($"{Name} is too young to drink wine at age {Age}");
}
}
class Program
{
static void Main()
{
var mike = new Person { Name = "Mike", Age = 20 };
mike.DrinkWine(2); // Nói với Person để uống, để nó tự xử lý logic
}
}
Lợi ích và rủi ro
So sánh giữa 2 version:
Tuy nhiên, cần lưu ý rủi ro: nếu class Person xử lý quá nhiều trách nhiệm (ví dụ, lưu vào cơ sở dữ liệu, thông báo với ba mẹ bạn Mike bạn này định uống rượu,...), lại có thể dẫn đến sự phụ thuộc cao (high coupling). Trong trường hợp này, nên tách biệt mối quan tâm, như giao nhiệm vụ lưu trữ, thông báo cho một lớp riêng, để tuân thủ nguyên tắc tách biệt (Separation of Concerns) - lại hẹn các bạn trao đổi về nguyên tắc này lần sau nha.
Giả sử chúng ta mô phỏng một đèn giao thông với các trạng thái: đỏ, vàng, xanh, và quyết định xe có thể đi qua hay không.
Ask version:
public class TrafficLight
{
public string CurrentColor { get; set; } // "Red", "Yellow", "Green"
public void ChangeColor(string newColor)
{
CurrentColor = newColor;
Console.WriteLine($"Traffic light changed to {CurrentColor}.");
}
}
class Program
{
static void Main()
{
var light = new TrafficLight { CurrentColor = "Red" };
if (light.CurrentColor == "Green") // Hỏi trạng thái và quyết định
{
Console.WriteLine("Vehicles can pass.");
}
else
{
Console.WriteLine("Vehicles must stop.");
}
}
}
Đoạn code trên vẫn không sai về business logic, nhưng vấn đề là:
Tell version:
public class TrafficLight
{
private string CurrentColor { get; set; }
public TrafficLight(string initialColor)
{
CurrentColor = initialColor;
}
public void ChangeColor(string newColor)
{
CurrentColor = newColor;
Console.WriteLine($"Traffic light changed to {CurrentColor}.");
}
public bool CanVehiclesPass()
{
return CurrentColor == "Green"; // Logic được đóng gói
}
}
class Program
{
static void Main()
{
var light = new TrafficLight("Red");
if (light.CanVehiclesPass()) // Nói và nhận phản hồi, không hỏi trực tiếp
{
Console.WriteLine("Vehicles can pass.");
}
else
{
Console.WriteLine("Vehicles must stop.");
}
}
}
Như ta thấy Tell version đã cải thiện được kha khá vấn đề
Better version: không cần điều kiện (if) ở mã khách
public class TrafficLight
{
private string CurrentColor { get; set; }
public TrafficLight(string initialColor)
{
CurrentColor = initialColor;
}
public void AllowTraffic()
{
if (CurrentColor == "Green")
{
Console.WriteLine("Vehicles can pass.");
}
else
{
Console.WriteLine("Vehicles must stop.");
}
}
}
class Program
{
static void Main()
{
var light = new TrafficLight("Red");
light.AllowTraffic(); // Chỉ nói, không hỏi
}
}
Giả sử chúng ta có một lớp Employee để tính lương dựa trên giờ làm việc và mức lương giờ.
Chủ đề này rất là hot nha :) lần này tôi sẽ chỉ đưa Tell version để các bạn tự cải thiện nhé.
Tell version
public class Employee
{
public double HoursWorked { get; set; }
public double HourlyRate { get; set; }
public string Name { get; set; }
public void ReceivePayment(double amount)
{
Console.WriteLine($"{Name} received {amount:C}.");
}
}
class Program
{
static void Main()
{
var emp = new Employee { Name = "Alice", HoursWorked = 40, HourlyRate = 25.0 };
double salary = emp.HoursWorked * emp.HourlyRate; // Hỏi và tính toán bên ngoài
emp.ReceivePayment(salary);
}
}
Các bạn có thể giúp tôi đưa ra vấn đề của lớp Employee không? và cách cải thiện như thế nào
Giả sử chúng ta có một hệ thống thương mại điện tử, và cần kiểm tra xem một đơn hàng (Order) có thể được giao hàng dựa trên trạng thái thanh toán.
Tell version:
public class Order
{
public decimal TotalAmount { get; set; }
public bool IsPaid { get; set; }
public void Ship()
{
Console.WriteLine($"Order worth {TotalAmount} has been shipped.");
}
}
class Program
{
static void Main()
{
var order = new Order { TotalAmount = 100.0m, IsPaid = false };
if (order.IsPaid) // Hỏi trạng thái và quyết định bên ngoài
{
order.Ship();
}
else
{
Console.WriteLine("Cannot ship: Order is not paid.");
}
}
}
Vấn đề và cách cải thiện bài toán này của bạn là gì :) Hãy cho tôi biết trong comment nhé.
Nguyên tắc Tell Don't Ask/Information Expert là một công cụ mạnh mẽ để viết mã sạch, dễ bảo trì. Bằng cách để đối tượng tự xử lý logic của mình, chúng ta giảm sự phụ thuộc, tăng tính đóng gói, và làm cho hệ thống dễ phát triển hơn. Luôn cố gắng nói/tell thay vì hỏi/ask, và giao trách nhiệm cho các "chuyên gia" - là lớp có dữ liệu liên quan.
Hi vọng với lý thuyết và bài toán ví dụ trên đây giúp các bạn viết code tốt hơn.
/Son Do - believe in basic
#SoftwareDesign #TellDontAsk #CleanCode #DotNet #OOP #CSharp #SoftwareEngineering
#wecommit100xshare #1percentbetter