Lõi Quyết Định, Vỏ Không Quyết Định: Cách Tư Duy Mới Về Kiểm Thử Phần Mềm
Ý tưởng "Deterministic Core, Non-Deterministic Shell" mở rộng khái niệm Functional Core, Imperative Shell của Gary Bernhardt, giúp lập trình viên nhận diện và tách mã quyết định ra khỏi mã giao tiếp với thế giới bên ngoài. Cách tiếp cận này giữ nguyên lợi ích kiểm thử của lập trình hàm thuần túy nhưng mở rộng khả năng áp dụng cho mọi ngôn ngữ, kể cả C hay các hệ thống legacy phức tạp.

Trong giới lập trình, cái tên Gary Bernhardt gắn liền với một trong những ý tưởng kiến trúc có ảnh hưởng nhất: Functional Core, Imperative Shell (Lõi hàm, Vỏ mệnh lệnh). Mười bốn năm sau khi khái niệm này ra đời, một biến thể mới mang tên "Deterministic Core, Non-Deterministic Shell" (Lõi quyết định, Vỏ không quyết định) đang thu hút sự chú ý trên Hacker News, hứa hẹn mở rộng khả năng áp dụng cho cả những ngôn ngữ và hệ thống mà lập trình hàm thuần túy không phải lựa chọn thực tế.
Nhắc lại về mô hình Lõi Hàm – Vỏ Mệnh Lệnh
Ý tưởng gốc chia mã nguồn thành hai phần rõ ràng:
- Lõi hàm (Functional Core): thuần túy về mặt chức năng, không có IO, không có cập nhật trạng thái phá hủy. Đây là nơi chứa logic nghiệp vụ của ứng dụng.
- Vỏ mệnh lệnh (Imperative Shell): ít nhánh rẽ, nhưng duy trì trạng thái, điều phối các phụ thuộc bên ngoài và giao tiếp với thế giới thực — tức là IO.
Vỏ có nhiệm vụ truy vấn lõi bằng các giá trị, nhận kết quả trả về như một "hộp đen", rồi dùng kết quả đó để tương tác với bên ngoài: ghi vào cơ sở dữ liệu, gửi request, hay cập nhật giao diện người dùng.
Sự phân tách này có ý nghĩa thực tiễn lớn: lõi rất dễ kiểm thử vì cùng một đầu vào luôn cho cùng một đầu ra, không cần mock hay stub bất cứ thứ gì.
Từ "Hàm Thuần" sang "Quyết Định"
Điểm mấu chốt của bài viết nằm ở chỗ: thứ khiến lõi dễ kiểm thử không hẳn là tính thuần hàm, mà là tính quyết định (deterministic). Một hàm thuần túy luôn trả về cùng kết quả với cùng đầu vào — nhưng không chỉ hàm thuần túy mới có được đặc tính đó.
Tác giả đưa ra ví dụ so sánh một hàm cộng dồn đơn giản với một class máy trạng thái (state machine):
function add(ns) {
return ns.reduce((a, b) => a + b, 0)
}
class AddMachine {
#state = 0
transition(input) { this.#state += input }
get state() { return this.#state }
}
Hàm add dễ suy luận vì thuần túy. Nhưng AddMachine cũng hoàn toàn quyết định: cùng một chuỗi lệnh gọi transition, trạng thái cuối cùng luôn giống hệt nhau. Việc nó mang phong cách mệnh lệnh không làm thay đổi bản chất đó.
Đây chính là bước ngoặt: nếu nới lỏng yêu cầu từ "thuần hàm" xuống "quyết định", ta giữ được lợi ích kiểm thử của mô hình gốc mà vẫn áp dụng được rộng rãi — kể cả trong C, nơi lập trình hàm thuần túy gần như không khả thi.
Nhận Diện Tính Không Quyết Định
Tác giả gợi ý một cách tiếp cận thực dụng: thay vì cố định nghĩa thế nào là "quyết định", hãy bắt đầu từ những gì không quyết định. Các ví dụ điển hình gồm:
- Gọi RNG (bộ sinh số ngẫu nhiên) không được khởi tạo hạt giống
- Tác vụ bất đồng bộ và đa luồng
- Giao tiếp qua mạng
- Giao tiếp với tiến trình khác
- Đọc/ghi lưu trữ cục bộ
- Tương tác cơ sở dữ liệu
- Hỏi hệ điều hành về ngày giờ
Tất cả những thứ này thuộc về lớp vỏ không quyết định. Mỗi khi chúng xuất hiện trong logic nghiệp vụ, đó là dấu hiệu tự nhiên cho việc "chống phân mảnh" (defragmentation): tách hàm làm hai quanh điểm đó, hoặc nâng kết quả lên một tầng và truyền vào như tham số.
Minh họa quá trình chống phân mảnh đĩa cứng
Hình ảnh này gợi nhớ đến công cụ Disk Defragmenter trên Windows thời xưa — công cụ gom các mảnh dữ liệu nằm rải rác trên đĩa quay thành khối liền mạch. Trong bối cảnh phần mềm, việc "chống phân mảnh tính quyết định" cũng mang lại cảm giác thỏa mãn tương tự.
Làm Việc Với Những Gì Bạn Đang Có
Với những ai đang ngày ngày vật lộn trong các dự án legacy hoặc mã do AI sinh ra, lời khuyên "hãy viết hàm thuần túy" nghe có vẻ xa vời. Tác giả thừa nhận điều này hoàn toàn chính đáng — không phải ai cũng may mắn như đội ngũ FoundationDB, những người ngay từ ngày đầu đã thiết kế hệ thống theo nguyên lý này.
Nhưng thay vì để cái hoàn hảo cản trở cái tốt, tác giả đề xuất một góc nhìn tích cực: hãy hình dung mỗi codebase trung bình đều chứa hàng ngàn lõi quyết định nhỏ bé, rải rác như những ngôi sao trên trời. Cách nhìn bi quan là đó là một mớ hỗn độn không thể cứu vãn. Cách nhìn lạc quan là bên trong vẫn có nhiều lõi quyết định giá trị, chỉ là chúng chưa được gom lại.
Đừng để cái hoàn hảo trở thành kẻ thù của cái tốt — hãy nhận diện tính quyết định ở bất cứ đâu bạn có thể, dù chỉ là vài dòng trong một hàm lớn, rồi bắt đầu thu gom chúng lại.
Quá trình này có thể thực hiện dần dần: xác định các phần quyết định trong tệp, lớp, hoặc thậm chí vài dòng code, rồi tập hợp chúng. Càng nhiều tính quyết định được nhóm lại, diện tích bề mặt mã "khó kiểm thử" càng thu hẹp. Trên một codebase lớn, bạn có thể sẽ không bao giờ đạt tới một lõi quyết định duy nhất — nhưng có hàng trăm lõi vẫn tốt hơn hàng ngàn mảnh vụn rải rác.
Đánh Thức Cỗ Máy Trạng Thái Trong Bạn
Thông điệp xuyên suốt của bài viết khá rõ ràng: mọi codebase hỗn độn đều chứa một hoặc nhiều máy trạng thái quyết định ẩn bên trong, dù điều đó không hề hiển nhiên. Tác giả cam kết chúng chắc chắn tồn tại.
Và một khi đã tìm ra, bạn sẽ ngạc nhiên vì phần mềm trở nên dễ sửa đổi và kiểm thử hơn bao nhiêu. Từng mảnh một, độ tin cậy của hệ thống có thể được tạo dựng lại từ đầu.
Với các lập trình viên Việt Nam đang làm việc trong các hệ thống cũ kỹ hoặc các dự án outsource đòi hỏi sửa chữa liên tục, cách tiếp cận "Lõi quyết định, Vỏ không quyết định" là một chiến lược cải thiện dần dần không cần đập đi xây lại — vừa thực tế, vừa không đòi hỏi thay đổi ngôn ngữ hay nền tảng.


