Domain Languages — Ngôn ngữ của riêng bài toán mình (Pragmatic Programmer #14)

Phong

Mở đầu

Mấy bạn ơi, mình lại lên đây tiếp tục series "The Pragmatic Programmer" rồi nè. Cuốn này đọc càng về sau càng thấm, thiệt sự luôn. Hôm nay tụi mình sẽ bàn về một chủ đề mà hồi lần đầu đọc mình thấy hơi "lạ" nhưng càng ngẫm càng thấy ngầu: Domain Languages – hay nói nôm na là "ngôn ngữ của riêng bài toán mình".

Lập trình viên đang viết code Ảnh: Lukas Blazek — Pexels

Domain Languages

Hồi xưa mình hay nghĩ lập trình là chuyển yêu cầu thành code, xài một ngôn ngữ như Python, Java, Go — vậy là đủ rồi. Nhưng mà cuốn sách này mở mắt mình ra một điều: đôi khi bạn cần tự tạo ra một ngôn ngữ nhỏ riêng cho bài toán của mình để code diễn tả ý đồ rõ ràng hơn.

Nghe có vẻ "khủng" nhưng thiệt ra nó đơn giản hơn mình nghĩ. Một ngôn ngữ domain-specific không nhất thiết phải có parser, lexer, compiler gì đâu. Nó có thể chỉ là một loạt function, một config file có cấu trúc, hay một mini DSL (Domain-Specific Language) viết bằng chính ngôn ngữ đang xài.

Internal vs External DSL

Sách chia làm hai loại:

Internal DSL — mình xài chính ngôn ngữ chủ để tạo ra cú pháp riêng. Ví dụ:

# Internal DSL cho rule engine
when(customer.total_purchases > 1000000)
  .then(apply_discount(0.15))
  .and(send_vip_email())

# Thay vì viết cả tá if-else lộn xộn
// C# example
var query = db.Users
  .Where(u => u.Age > 18)
  .OrderBy(u => u.Name)
  .Select(u => new { u.Name, u.Email });

Khoan — LINQ trong C# cũng là một internal DSL chứ không phải feature ngôn ngữ thông thường à? Đúng vậy! Tác giả chỉ ra rằng nhiều thứ mình tưởng là syntax bình thường hóa ra là ứng dụng của concept này.

External DSL — mình tạo ra một ngữ pháp riêng, đọc từ file config. Ví dụ quen thuộc nhất là YAML cho CI/CD, regex, SQL, hay HTML. Ngôn ngữ riêng hoàn toàn, có parser xịn sò.

Tại sao nên xài DSL?

Lợi ích mình thấy từ đọc topic này:

  1. Code gần với domain hơn — Thay vì code toàn technical implementation, mình viết bằng từ ngữ của business. Ai đọc cũng hiểu, kể cả BA hay PO.

  2. Dễ thay đổi logic — Config DSL có thể sửa mà không cần recompile, deploy lại. Rất tiện cho mấy rule business thay đổi liên tục.

  3. Tách biệt policy khỏi implementation — Business logic (viết bằng DSL) độc lập với code nền (framework, library). Cái này là một trong những nguyên lý lõi của sách: "Don't program by coincidence".

Trong thực tế mình thấy gì?

Mình từng làm một dự án có cả tá rule tính phí ship — nào là free ship cho đơn > 500k, ship ưu tiên cho VIP, phí theo khu vực, discount theo giờ... Ban đầu tụi mình xài cả đống if-else, mỗi lần thay đổi là phải sửa code, review, deploy. Rất mệt.

Sau đó mình học được cái này và chuyển qua dùng một JSON config:

{
  "rules": [
    {"condition": "total > 500000", "action": "free_shipping"},
    {"condition": "customer.vip && total < 500000", "action": "discount_50"},
    {"condition": "hour < 8 || hour > 22", "action": "surcharge_10"}
  ]
}

Mỗi lần business muốn thay đổi — chỉ cần sửa file config, đẩy lên. Không cần build, không cần deploy. Rule phức tạp hơn thì viết một script engine nhỏ để evaluate condition trong code.

Công việc lập trình Ảnh: ThisisEngineering — Pexels

Lưu ý

DSL cũng có cái giá của nó. Tác giả cũng nói rõ: đừng làm DSL nếu bài toán của bạn đơn giản. Thêm một lớp trừu tượng luôn đi kèm với complexity. Hãy tự hỏi:

  • Có cần thiết không? Nếu thay đổi hiếm khi xảy ra thì khỏi làm DSL.
  • Có làm code khó hiểu hơn không? Một DSL dở còn tệ hơn code bình thường.
  • Ai sẽ đọc/viết DSL này? Nếu chỉ mỗi mình bạn hiểu thì vô dụng.

Kết luận

Topic 14 này dạy mình biết khi nào xài DSL, khi nào không. Internal DSL làm code sạch hơn khi áp dụng cho business logic. External DSL giúp tách biệt policy khỏi implementation, làm hệ thống linh hoạt hơn.

Cuốn sách càng ngày càng cho mình thấy: code tốt không chỉ là chạy đúng, mà còn là diễn tả được ý đồ một cách rõ ràng. Domain Language là một công cụ để làm điều đó.

Hẹn mấy bạn ở bài sau nha! 📖