Nguyên tắc Tell Don't Ask/Information Expert trong lập trình

Công nghệ - 02/04/2025 05:00:17

💡Viết code đúng chất OOP với nguyên tắc "Tell Don't Ask"! Đừng lấy dữ liệu rồi xử lý bên ngoài, hãy để chính object chuyên gia thực thi. Cách làm này giúp code gọn, ít lỗi, dễ bảo trì—bạn đã áp dụng chưa?

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é.

Giới thiệu về nguyên tắc Tell Don't Ask/Information Expert

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.

Nguồn gốc và tầm quan trọng

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ư:

  • Feature Envy: Mã khách (hay client code) phụ thuộc quá nhiều vào chi tiết nội bộ của đối tượng. Mã khách ở đây ta có thể tạm hiểu là bạn A dùng service của bạn B viết, mà không quan tâm cụ thể tới bạn B viết gì, chỉ cần biết đầu hàm và tham số đầu vào, đầu ra.
  • Lặp lại logic: Nếu logic được viết lại ở nhiều nơi, việc bảo trì trở nên phức tạp, vi phạm nguyên tắc DRY (Don't Repeat Yourself).
  • Mô hình miền thiếu sức sống (Anemic Domain Model): Đối tượng chỉ chứa dữ liệu, không có hành vi, làm giảm tính hướng đối tượng.
  • ...

Ngược lại, áp dụng TdA/IE mang lại lợi ích như:

  • Tăng tính đóng gói: Đối tượng tự quản lý trạng thái và hành vi.
  • Giảm sự phụ thuộc: Mã khách không cần biết chi tiết nội bộ, giúp dễ dàng thay đổi mà không ảnh hưởng đến các phần khác
  • Dễ kiểm thử và bảo trì: Logic được tập trung, giảm lặp lại.

Để 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é.


Ví dụ minh họa trong C#

Ví dụ 1: Kiểm tra độ tuổi uống rượu

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:

  • Ask version: Tạo sự phụ thuộc chặt chẽ, khó bảo trì nếu logic lặp lại, và lộ thông tin công khai (như thuộc tính DrinkingAge) có thể dẫn đến lạm dụng để sửa đổi từ phía ngoài.
  • Tell version: Giảm sự phụ thuộc, dễ bảo trì, và giảm rủi ro "Feature Envy".

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.

 

Ví dụ 2: Điều khiển đèn giao thông (Traffic Light)

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à:

  • Program phải biết ý nghĩa của CurrentColor và tự quyết định logic.
  • Nếu thêm trạng thái mới (ví dụ, "Blinking Yellow"), logic kiểm tra phải được cập nhật ở mọi nơi.
  • Dễ xảy ra lỗi do so sánh chuỗi thủ công.

 

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 đề

  • Logic kiểm tra được đóng gói trong TrafficLight, không rò rỉ ra ngoài.
  • Nếu logic thay đổi (ví dụ, "Blinking Yellow" cũng cho phép đi qua), chỉ cần sửa trong CanVehiclesPass.
  • Mã khách chỉ cần gọi phương thức, không cần biết chi tiết trạng thái.

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
    }
}

  • Loại bỏ hoàn toàn if ở mã khách, tuân thủ TdA chặt chẽ hơn.
  • Tất cả logic nằm trong TrafficLight, tăng tính kết dính.

 

Bài tập 1: Tính toán tiền lương (Payroll Calculation)

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

Bài tập 2: Quản lý đơn hàng (Order Processing)

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é.


Tổng kết

  • Vi phạm TdA/IE: Logic rò rỉ ra mã khách, tạo sự phụ thuộc chặt chẽ, khó bảo trì, và dễ lặp lại.
  • Áp dụng TdA/IE: Logic được đóng gói trong lớp sở hữu dữ liệu, tăng tính kết dính, giảm phụ thuộc, và dễ mở rộng.

 

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

Nguồn tham khảo

 

Công nghệ

Xem tất cả