Dijkstra & Lập trình có cấu trúc: Kỷ luật có khả năng mở rộng
Những ý tưởng lập trình có cấu trúc của Edsger Dijkstra giải thích tại sao mã có kỷ luật và đơn giản giữ được đúng đắn và dễ bảo trì khi đội, tính năng và hệ thống mở rộng.

Tại sao Dijkstra vẫn quan trọng khi phần mềm phát triển
Phần mềm hiếm khi thất bại vì không thể viết được. Nó thất bại vì, một năm sau, không ai có thể thay đổi nó một cách an toàn.
Khi mã nguồn lớn lên, mỗi thay đổi “nhỏ” bắt đầu lan rộng: một sửa lỗi làm vỡ một tính năng xa xôi, một yêu cầu mới buộc phải viết lại, và một refactor đơn giản biến thành hàng tuần phối hợp cẩn thận. Phần khó không phải là thêm mã—mà là giữ hành vi có thể dự đoán được trong khi mọi thứ xung quanh nó thay đổi.
Lời hứa: tính đúng đắn và đơn giản giảm chi phí dài hạn
Edsger Dijkstra cho rằng tính đúng đắn và sự đơn giản nên là mục tiêu hàng đầu, không chỉ là thứ hay ho. Lợi ích không phải là học thuật. Khi hệ thống dễ suy luận hơn, các đội dành ít thời gian dập lửa và nhiều thời gian hơn để xây dựng.
- Tính đúng đắn giảm chi phí sai sót: ít sự cố, ít hồi quy, ít mã dạng “đừng động vào đó”.
- Đơn giản giảm chi phí thay đổi: ít giả định ẩn, ít trường hợp đặc biệt, ít bất ngờ khi review.
“Scale” thực sự nghĩa là gì
Khi người ta nói phần mềm cần “scale”, họ thường nghĩ đến hiệu năng. Điểm của Dijkstra khác: độ phức tạp cũng nhân lên.
Scale xuất hiện dưới dạng:
- Nhiều tính năng hơn: luồng mới, các trường hợp biên, và kỳ vọng người dùng.
- Nhiều người hơn: chuyển giao, phong cách khác nhau, và bối cảnh biến động.
- Nhiều tích hợp hơn: API bên ngoài, nguồn dữ liệu, và chế độ lỗi.
- Nhiều thời gian hơn: quyết định di sản, yêu cầu thay đổi, và sửa từng phần.
Ý tưởng cốt lõi: cấu trúc làm hành vi dễ dự đoán hơn
Lập trình có cấu trúc không phải là khắt khe vì mục đích khắt khe. Nó là lựa chọn luồng điều khiển và phân tách sao cho dễ trả lời hai câu hỏi:
- “Tiếp theo xảy ra gì?”
- “Trong điều kiện nào?”
Khi hành vi có thể dự đoán, thay đổi trở nên thường nhật thay vì rủi ro. Đó là lý do Dijkstra vẫn quan trọng: kỷ luật của ông nhắm vào nút thắt thực sự khi phần mềm lớn lên—hiểu đủ tốt để cải thiện nó.
Giới thiệu ngắn gọn về Edsger Dijkstra và mục tiêu của ông
Edsger W. Dijkstra (1930–2002) là nhà khoa học máy tính người Hà Lan, người góp phần định hình cách lập trình viên suy nghĩ về việc xây phần mềm đáng tin cậy. Ông làm việc trên các hệ điều hành sớm, đóng góp cho các thuật toán (bao gồm thuật toán đường đi ngắn mang tên ông), và—quan trọng nhất với lập trình viên hàng ngày—thúc đẩy ý tưởng rằng lập trình nên là thứ ta có thể suy luận được, không chỉ thử đến khi nó có vẻ chạy.
Trọng tâm cốt lõi: suy luận thay vì “chạy trên máy tôi”
Dijkstra ít quan tâm liệu một chương trình có thể được làm chạy đúng với vài ví dụ, hơn là liệu ta có thể giải thích tại sao nó đúng với các trường hợp quan trọng hay không.
Nếu bạn có thể phát biểu mã đó phải làm gì, bạn nên có thể lập luận (từng bước) rằng nó thực sự làm được điều đó. Tư duy này dẫn đến mã dễ theo dõi hơn, dễ review hơn và ít phụ thuộc vào debug mang tính anh hùng.
Tại sao ông có thể nghe khắt khe—và tại sao điều đó hữu ích
Một số bài viết của Dijkstra đọc khá kiên quyết. Ông chỉ trích các mẹo “thông minh”, luồng điều khiển lộn xộn và thói quen viết mã làm suy giảm khả năng suy luận. Sự khắt khe không phải để bắt lỗi phong cách; mà là để giảm mơ hồ. Khi ý nghĩa mã rõ ràng, bạn tốn ít thời gian tranh luận về ý định và nhiều thời gian hơn để xác thực hành vi.
Lập trình có cấu trúc nghĩa là gì (tổng quan)
Lập trình có cấu trúc là thực hành xây chương trình từ một tập nhỏ các cấu trúc điều khiển rõ ràng—trình tự, lựa chọn (if/else), và lặp—thay vì những cú nhảy luồng rối rắm. Mục tiêu đơn giản: làm đường đi qua chương trình dễ hiểu để bạn có thể giải thích, bảo trì và thay đổi nó với tự tin.
Tính đúng đắn: tính năng im lặng mà người dùng dựa vào
Người ta thường mô tả chất lượng phần mềm là “nhanh”, “đẹp”, hay “nhiều tính năng”. Người dùng trải nghiệm tính đúng đắn theo cách khác: như sự tự tin lặng lẽ rằng ứng dụng sẽ không làm họ bất ngờ. Khi tính đúng đắn có mặt, chẳng ai để ý. Khi nó thiếu, mọi thứ khác trở nên vô nghĩa.
“Nó chạy bây giờ” vs “nó tiếp tục chạy”
“Nó chạy bây giờ” thường có nghĩa bạn đã thử vài luồng và nhận kết quả mong đợi. “Nó tiếp tục chạy” nghĩa là nó hành xử như dự định theo thời gian, các trường hợp biên và thay đổi—sau refactor, tích hợp mới, lưu lượng cao hơn và người mới chạm vào mã.
Một tính năng có thể “chạy bây giờ” nhưng vẫn mong manh:
- Nó phụ thuộc vào việc input luôn sạch.
- Nó giả định một cuộc gọi mạng luôn trả lời nhanh.
- Nó vượt qua các test chỉ che phủ đường đi thuận lợi.
Tính đúng đắn là xoá các giả định ẩn này—hoặc làm chúng rõ ràng.
Cách lỗi nhỏ nhân lên trong hệ thống lớn
Một lỗi nhỏ hiếm khi vẫn nhỏ khi phần mềm phát triển. Một trạng thái không đúng, một sai lệch một đơn vị, hay một quy tắc xử lý lỗi không rõ sẽ bị sao chép vào module mới, được bọc bởi dịch vụ khác, cached, retry, hoặc “vá tạm”. Theo thời gian, các đội ngừng hỏi “điều gì là đúng?” và bắt đầu hỏi “thường xảy ra gì?” Đó là khi phản ứng sự cố biến thành khảo cổ.
Bộ nhân là phụ thuộc: một hành vi sai nhỏ trở thành nhiều hành vi sai hạ nguồn, mỗi hành vi có bản vá cục bộ riêng.
Rõ ràng là công cụ của tính đúng đắn (không chỉ là lựa chọn phong cách)
Mã rõ ràng cải thiện tính đúng đắn vì nó cải thiện giao tiếp:
- Code review bắt được vấn đề thực khi ý định rõ ràng.
- Onboarding nhanh hơn khi quy tắc đọc được, không phải tri thức bộ tộc.
- Sự cố được giải quyết nhanh hơn khi luồng điều khiển và cơ chế lỗi dễ theo dõi.
Định nghĩa thực tế của tính đúng đắn cho các đội sản phẩm
Tính đúng đắn có nghĩa: với các đầu vào và tình huống chúng ta cam kết hỗ trợ, hệ thống nhất quán tạo ra kết quả mà chúng ta hứa—và khi không thể, nó thất bại theo cách có thể dự đoán và giải thích.
Đơn giản như một chiến lược, không chỉ là sở thích phong cách
Đơn giản không phải làm mã “xinh xắn”, ít dòng, hay mẹo thông minh. Nó là làm hành vi dễ dự đoán, giải thích và sửa đổi mà không sợ. Dijkstra trân trọng sự đơn giản vì nó tăng khả năng suy luận về chương trình—đặc biệt khi mã và đội phát triển.
Đơn giản là gì (và không phải là gì)
Mã đơn giản giữ một số ít ý tưởng đang vận hành cùng lúc: luồng dữ liệu rõ ràng, luồng điều khiển rõ ràng, và trách nhiệm rõ ràng. Nó không bắt người đọc phải mô phỏng nhiều đường đi thay thế trong đầu.
Đơn giản không phải:
- Ít dòng bằng mọi giá
- Mẹo “thông minh”, các một dòng dày đặc, hay abstraction nặng
- Tránh cấu trúc để trông linh hoạt
Độ phức tạp ngẫu nhiên: những gì bạn vô tình thêm vào
Nhiều hệ thống trở nên khó thay đổi không phải vì miền vấn đề vốn phức tạp, mà vì chúng ta thêm độ phức tạp tình cờ: cờ điều khiển tương tác bất ngờ, bản vá trường hợp đặc biệt không bao giờ bị xoá, và lớp lớp tồn tại chủ yếu để làm việc quanh quyết định trước đó.
Mỗi ngoại lệ là một khoản thuế trên việc hiểu biết. Chi phí xuất hiện sau này, khi ai đó cố sửa lỗi và phát hiện một thay đổi ở chỗ này vô tình phá vài chỗ khác.
Thiết kế đơn giản giảm nhu cầu anh hùng cứu nguy
Khi thiết kế đơn giản, tiến triển đến từ công việc đều đặn: thay đổi có thể review, diff nhỏ, và ít sửa khẩn cấp. Đội không cần những lập trình viên “anh hùng” nhớ mọi trường hợp biên lịch sử hay debug dưới áp lực lúc 2 giờ sáng. Thay vào đó, hệ thống hỗ trợ sự chú ý bình thường của con người.
Quy tắc ngón tay: ít trường hợp đặc biệt hơn, ít bất ngờ hơn
Một bài kiểm tra thực tế: nếu bạn tiếp tục thêm ngoại lệ (“trừ khi…”, “ngoại trừ khi…”, “chỉ cho khách hàng này…”), bạn có lẽ đang tích lũy độ phức tạp tình cờ. Ưu tiên giải pháp giảm phân nhánh hành vi—một quy tắc nhất quán đánh bại năm trường hợp đặc biệt, ngay cả khi quy tắc nhất quán hơi tổng quát hơn bạn tưởng tượng ban đầu.
Lập trình có cấu trúc: luồng điều khiển rõ ràng bạn có thể tin tưởng
Lập trình có cấu trúc là ý tưởng đơn giản nhưng hậu quả lớn: viết mã để đường đi thực thi dễ theo dõi. Nói ngắn gọn, hầu hết chương trình có thể xây từ ba khối xây dựng—sequence, selection, và repetition—mà không dựa vào các cú nhảy rối rắm.
Ba khối xây dựng (bằng ngôn ngữ con người)
- Sequence: làm bước A, rồi B, rồi C.
- Selection: chọn một nhánh dựa trên điều kiện (ví dụ,
if/else,switch). - Repetition: lặp một tập bước khi điều kiện còn đúng (ví dụ,
for,while).
Khi luồng điều khiển được cấu thành từ các cấu trúc này, bạn thường có thể giải thích chương trình bằng cách đọc từ trên xuống, mà không phải “dịch chuyển” khắp file.
Nó thay thế gì: các đường dẫn spaghetti
Trước khi lập trình có cấu trúc trở thành chuẩn, nhiều codebase phụ thuộc nhiều vào các cú nhảy tùy ý (kiểu goto). Vấn đề không phải là cú nhảy luôn xấu; mà là nhảy không bị giới hạn tạo ra các đường đi thực thi khó dự đoán. Bạn kết thúc với những câu hỏi như “Chúng ta đến đây bằng cách nào?” và “Biến này ở trạng thái nào?”—và mã không trả lời rõ ràng.
Tại sao rõ ràng quan trọng với các đội thực tế
Luồng điều khiển rõ ràng giúp con người xây một mô hình tinh thần đúng đắn. Mô hình đó là thứ bạn dựa vào khi debug, review pull request, hoặc thay đổi hành vi dưới áp lực thời gian.
Khi cấu trúc nhất quán, việc sửa đổi an toàn hơn: bạn có thể thay đổi một nhánh mà không vô tình ảnh hưởng nhánh khác, hoặc refactor một vòng lặp mà không bỏ sót một lối thoát ẩn. Độ đọc không chỉ là thẩm mỹ—nó là nền tảng để thay đổi hành vi một cách tự tin mà không phá vỡ cái đang hoạt động.
Công cụ suy luận: Invariants, Preconditions và Postconditions
Dijkstra thúc đẩy một ý tưởng đơn giản: nếu bạn có thể giải thích tại sao mã đúng, bạn có thể thay đổi nó ít lo lắng hơn. Ba công cụ suy luận nhỏ làm điều đó khả thi—mà không biến đội thành nhà toán học.
Invariants: “những sự thật luôn đúng”
Một invariant là một chân lý giữ nguyên khi một đoạn mã chạy, đặc biệt trong một vòng lặp.
Ví dụ: bạn đang cộng các giá trong giỏ hàng. Invariant hữu ích là: “total bằng tổng các mục đã xử lý”. Nếu điều đó luôn đúng ở mỗi bước, khi vòng lặp kết thúc, kết quả đáng tin cậy.
Invariants mạnh vì chúng tập trung chú ý vào những gì không bao giờ được sai, chứ không chỉ cái xảy ra tiếp theo.
Preconditions và postconditions: hợp đồng hàng ngày
Một precondition là điều phải đúng trước khi một hàm chạy. Một postcondition là điều hàm đảm bảo sau khi hoàn thành.
Ví dụ hàng ngày:
- Precondition: “Bạn chỉ rút tiền nếu tài khoản có đủ số dư.”
- Postcondition: “Sau rút, số dư giảm chính xác số đó, và không bao giờ âm.”
Trong mã, precondition có thể là “danh sách đầu vào đã được sắp xếp”, và postcondition có thể là “danh sách đầu ra đã sắp xếp và chứa các phần tử giống trước plus phần được chèn”.
Việc viết chúng ra thay đổi mã và review thế nào
Khi bạn viết những điều này ra (thậm chí một cách không chính thức), thiết kế sẽ rõ hơn: bạn quyết định hàm mong đợi gì và hứa gì, và bạn tự nhiên làm nó nhỏ hơn và tập trung hơn.
Trong review, cuộc tranh luận chuyển khỏi phong cách (“Tôi sẽ viết khác”) sang tính đúng đắn (“Hàm này có giữ invariant không?” “Chúng ta thi hành precondition hay chỉ ghi vào tài liệu?”).
Thực hành nhẹ nhàng: comment nơi lỗi thường xuất hiện
Bạn không cần chứng minh hình thức để hưởng lợi. Chọn vòng lặp lỗi nhất hoặc cập nhật trạng thái phức tạp nhất và thêm một comment invariant một dòng phía trên. Khi ai đó sửa sau này, comment đó như rào chắn: nếu thay đổi phá vỡ sự thật này, mã không còn an toàn.
Kiểm thử vs Suy luận: mỗi cái có thể và không thể đảm bảo gì
Kiểm thử và suy luận đều nhắm đến cùng mục tiêu—phần mềm hành xử như mong muốn—nhưng vận hành rất khác. Test phát hiện vấn đề bằng cách thử ví dụ. Suy luận ngăn chặn cả lớp vấn đề bằng cách làm logic rõ ràng và có thể kiểm tra được.
Test mạnh ở điểm gì
Test là lưới an toàn thực tế. Chúng bắt regressions, xác minh kịch bản thực tế, và ghi lại hành vi mong đợi để cả đội chạy.
Nhưng test chỉ có thể chứng tỏ sự tồn tại của lỗi, chứ không phải không có lỗi. Không bộ test nào bao phủ mọi đầu vào, mọi biến thể thời gian, hay mọi tương tác tính năng. Nhiều lỗi “chạy trên máy tôi” đến từ các kết hợp không được test: input hiếm, thứ tự thao tác đặc biệt, hoặc trạng thái tinh tế chỉ xuất hiện sau vài bước.
Suy luận có thể đảm bảo gì (và không thể)
Suy luận là về chứng minh thuộc tính của mã: “vòng lặp này luôn kết thúc”, “biến này không bao giờ âm”, “hàm này không bao giờ trả về đối tượng không hợp lệ”. Khi làm tốt, nó loại trừ cả lớp lỗi—đặc biệt quanh biên và các trường hợp khó.
Hạn chế là công sức và phạm vi. Chứng minh hình thức cho toàn bộ sản phẩm hiếm khi kinh tế. Suy luận hiệu quả nhất khi áp dụng có chọn lọc: thuật toán lõi, luồng nhạy cảm bảo mật, logic tiền tệ và đồng thời.
Cách tiếp cận cân bằng mà mở rộng được
Dùng test rộng, và áp dụng suy luận sâu nơi thất bại tốn kém.
Cầu nối thực tế giữa hai thứ là làm cho ý định có thể thi hành:
- Assertions cho giả định nội bộ (ví dụ, “index nằm trong phạm vi”).
- Preconditions và postconditions (hợp đồng) cho đầu vào/đầu ra hàm.
- Invariants cho sự thật liên tục (ví dụ, “tổng giỏ hàng bằng tổng các mục”).
Những kỹ thuật này không thay test—chúng làm lưới an toàn chặt hơn. Chúng biến mong đợi mơ hồ thành quy tắc có thể kiểm tra, khiến lỗi khó viết và dễ chẩn đoán hơn.
Kỷ luật: làm sao các đội tránh “nợ sự khéo léo”
Mã “khéo léo” thường cho cảm giác thắng lợi tức thì: ít dòng hơn, mẹo gọn, một dòng làm bạn thấy thông minh. Vấn đề là sự khéo léo không nhân rộng theo thời gian hay theo người. Sáu tháng sau, tác giả quên mẹo. Đồng đội mới đọc theo nghĩa đen, bỏ qua giả định ẩn và thay đổi theo cách phá vỡ hành vi. Đó là “nợ sự khéo léo”: tốc độ ngắn hạn mua bằng sự mơ hồ dài hạn.
Kỷ luật là gia tốc đội
Quan điểm của Dijkstra không phải “viết mã nhàm chán” như một sở thích—mà là ràng buộc có kỷ luật làm chương trình dễ suy luận hơn. Trên một đội, ràng buộc còn giảm mệt mỏi khi ra quyết định. Nếu mọi người đã biết mặc định (cách đặt tên, cấu trúc hàm, thế nào là “xong”), bạn khỏi phải tranh luận lại cơ bản trong mỗi PR. Thời gian đó trở lại cho công việc sản phẩm.
Kỷ luật xuất hiện trong các thực hành thường xuyên:
- Code review ưu tiên rõ ràng hơn mới lạ (“Người khác có thể thay đổi an toàn không?”).
- Tiêu chuẩn chung (định dạng, đặt tên, xử lý lỗi) để codebase đọc như một giọng nói.
- Refactor như bảo trì, không phải cứu nguy—dọn dẹp nhỏ liên tục.
“Có kỷ luật” trông như thế nào trong mã
Một vài thói quen cụ thể ngăn nợ sự khéo léo tích tụ:
- Hàm nhỏ làm một việc, với đầu vào và đầu ra rõ ràng.
- Tên rõ ràng thể hiện ý định (ưu tiên
calculate_total()hơndo_it()). - Không có trạng thái ẩn: giảm global và side effect; truyền phụ thuộc rõ ràng.
- Luồng điều khiển thẳng thắn: tránh logic phụ thuộc thứ tự tinh tế, giá trị ma thuật, hoặc “chỉ chạy nếu bạn biết mẹo.”
Kỷ luật không phải hoàn hảo—mà là làm cho lần thay đổi tiếp theo có thể dự đoán được.
Mô-đun hóa và ranh giới: giữ thay đổi ở phạm vi cục bộ
Mô-đun hóa không chỉ là “chia mã thành file.” Nó là cô lập quyết định sau ranh giới rõ ràng, sao cho phần còn lại của hệ thống không cần biết (hoặc quan tâm) về chi tiết bên trong. Một module ẩn phần lộn xộn—cấu trúc dữ liệu, các trường hợp biên, mẹo tối ưu—và chỉ phơi ra bề mặt nhỏ, ổn định.
Module thu nhỏ bán kính phá hoại thế nào
Khi có yêu cầu thay đổi, kết quả lý tưởng là: một module thay đổi, mọi thứ khác không chạm. Đó là ý nghĩa thực tế của “giữ thay đổi cục bộ.” Ranh giới ngăn coupling vô ý—nơi update một tính năng vô tình phá ba tính năng khác vì chúng chia sẻ giả định.
Một ranh giới tốt cũng làm suy luận dễ hơn. Nếu bạn có thể phát biểu module đảm bảo gì, bạn có thể suy luận về chương trình lớn hơn mà không cần đọc lại toàn bộ cài đặt mỗi lần.
Giao diện như lời hứa (và cách nó cho phép làm việc song song)
Giao diện là một lời hứa: “Với những đầu vào này, tôi sẽ tạo những đầu ra này và giữ những quy tắc này.” Khi lời hứa rõ ràng, các đội có thể làm việc song song:
- Một người triển khai module.
- Người khác xây caller dùng giao diện đó.
- QA thiết kế test quanh hành vi đã hứa.
Đây không phải quan liêu—mà là tạo điểm phối hợp an toàn trong codebase đang lớn.
Những kiểm tra module đơn giản ngăn trôi ranh giới
Bạn không cần review kiến trúc lớn để cải thiện modularity. Thử những kiểm tra nhẹ:
- Đầu vào/đầu ra: Bạn có liệt kê được đầu vào, đầu ra và side effect của module trong vài dòng không? Nếu không, nó có lẽ làm quá nhiều việc.
- Sở hữu: Ai chịu trách nhiệm hành vi và thay đổi? Module không người chủ biến thành bãi tập rác.
- Phụ thuộc: Nó phụ thuộc vào “mọi thứ” hay chỉ những gì cần? Ít phụ thuộc hơn có nghĩa ít phá vỡ bất ngờ.
Ranh giới vẽ tốt biến “thay đổi” từ sự kiện toàn hệ thống thành chỉnh sửa cục bộ.
Tại sao những ý tưởng này thắng khi mở rộng (đội, codebase và thời gian)
Khi phần mềm nhỏ, bạn có thể “ôm tất cả trong đầu.” Ở quy mô lớn, điều đó không còn đúng—và các chế độ thất bại trở nên quen thuộc.
Triệu chứng phổ biến như:
- Sự cố truy vết đến một góc trường hợp bất ngờ
- Phát hành chậm lại vì mọi thay đổi đều cảm thấy rủi ro
- Tích hợp mong manh nơi cập nhật nhỏ phá ba hệ thống hạ nguồn
Cấu trúc giảm tải nhận thức
Luận điểm cốt lõi của Dijkstra là con người là nút cổ chai. Luồng điều khiển rõ ràng, đơn vị nhỏ định nghĩa tốt và mã bạn có thể suy luận không phải là lựa chọn thẩm mỹ—chúng là bộ nhân công tăng công suất.
Trong codebase lớn, cấu trúc hoạt như nén để hiểu. Nếu hàm có đầu vào/đầu ra rõ ràng, module có ranh giới đặt tên được, và “đường đi thuận lợi” không rối cùng mọi trường hợp biên, lập trình viên tốn ít thời gian tái tạo ý định và nhiều thời gian hơn để thay đổi có chủ ý.
Nó mở rộng theo đội, không chỉ code
Khi đội lớn lên, chi phí giao tiếp tăng nhanh hơn số dòng mã. Mã có kỷ luật, dễ đọc giảm lượng tri thức bộ tộc cần để đóng góp an toàn.
Điều này thể hiện ngay trong onboarding: kỹ sư mới có thể theo các mẫu dự đoán, học một tập quy ước nhỏ, và thay đổi mà không cần tham quan dài về các “bẫy”. Mã tự nó dạy hệ thống.
Sự cố dễ gỡ hơn—và an toàn để hoàn tác
Trong sự cố, thời gian khan hiếm và tự tin dễ vỡ. Mã viết với giả định rõ ràng (preconditions), kiểm tra có ý nghĩa và luồng điều khiển thẳng thắn dễ lần theo khi áp lực.
Quan trọng không kém, thay đổi có kỷ luật dễ rollback. Các chỉnh sửa nhỏ, cục bộ với ranh giới rõ ràng giảm khả năng rollback gây lỗi mới. Kết quả không phải là hoàn hảo—mà là ít bất ngờ hơn, phục hồi nhanh hơn, và hệ thống vẫn bảo trì được khi thời gian và người đóng góp tích lũy.
Áp dụng Dijkstra mà không giáo điều
Điểm của Dijkstra không phải “viết mã theo lối xưa.” Mà là “viết mã bạn có thể giải thích.” Bạn có thể nhận tư duy đó mà không biến mọi tính năng thành bài toán chứng minh hình thức.
Biến nguyên tắc thành thói quen hàng ngày
Bắt đầu với các lựa chọn làm suy luận rẻ:
- Ưu tiên luồng điều khiển đơn giản: vài hàm nhỏ hơn một routine nhiều nhánh.
- Giảm side effect: giữ mutation gần nơi cần và đừng để hàm thay đổi trạng thái global lặng lẽ.
- Dùng hợp đồng rõ ràng: làm đầu vào, đầu ra và xử lý lỗi rõ ràng (bằng kiểu, tên và comment).
Một heuristic tốt: nếu bạn không thể tóm tắt điều hàm đảm bảo trong một câu, nó có lẽ làm quá nhiều.
“Nâng cấp cấu trúc” nhỏ (không rewrite)
Bạn không cần sprint refactor lớn. Thêm cấu trúc ở các mối nối:
- Trích một vòng lặp phức tạp thành hàm có tên và định nghĩa điều gì vẫn đúng mỗi vòng.
- Thay điều kiện “ma thuật” bằng predicate có tên rõ (ví dụ,
isEligibleForRefund). - Bao bọc chuyển trạng thái phức tạp sau một hàm duy nhất để phần còn lại không lạm dụng.
Những nâng cấp này là từng bước: chúng giảm tải nhận thức cho lần thay đổi tiếp theo.
Lời nhắc trong code review giữ bạn ngay thẳng
Khi review (hoặc viết) thay đổi, hỏi:
- “Điều gì phải đúng ở đây?” (invariants, giả định, trạng thái yêu cầu)
- “Điều gì có thể thay đổi an toàn?” (phần nào được phép biến đổi mà không phá caller)
Nếu reviewer không trả lời nhanh, mã đang báo hiệu phụ thuộc ẩn.
Ghi lại lý do, không chỉ các bước
Comment lặp lại mã sẽ trở nên lỗi thời. Thay vào đó, viết tại sao mã đúng: giả định chính, các trường hợp biên bạn bảo vệ, và điều gì sẽ hỏng nếu giả định thay đổi. Một ghi chú ngắn như “Invariant: total luôn bằng tổng các mục đã xử lý” giá trị hơn một đoạn mô tả dài.
Nếu cần chỗ nhẹ để lưu những thói quen này, gom chúng vào checklist chung (xem /blog/practical-checklist-for-disciplined-code).
Vai trò của trợ giúp AI (mà vẫn giữ kỷ luật)
Các đội hiện đại ngày càng dùng AI để tăng tốc giao hàng. Rủi ro quen thuộc: tốc độ hôm nay có thể biến thành sự rối rắm ngày mai nếu mã được sinh khó giải thích.
Cách dùng AI phù hợp với tinh thần Dijkstra là coi nó như chất xúc tác cho suy nghĩ có cấu trúc, không phải thay thế. Ví dụ, khi xây trong Koder.ai—nền tảng vibe-coding nơi bạn tạo web, backend và app di động qua chat—bạn có thể giữ thói quen “suy luận trước” bằng cách làm rõ prompt và bước review:
- Yêu cầu hợp đồng rõ ràng: “Định nghĩa preconditions, postconditions và hành vi lỗi cho endpoint này.”
- Yêu cầu invariants cho luồng có trạng thái: “Điều gì phải luôn đúng sau mỗi bước của state machine thanh toán này?”
- Dùng planning mode để ép phân tách thành các mảnh nhỏ, có thể review (module, interface, trách nhiệm) trước khi sinh cài đặt.
- Dựa vào snapshots và rollback để giữ thay đổi nhỏ và có thể đảo ngược—phản chiếu kỷ luật của chỉnh sửa cục bộ và đường hoàn tác an toàn.
Ngay cả khi bạn xuất mã nguồn và chạy ở nơi khác, nguyên lý vẫn vậy: mã sinh ra phải là mã bạn có thể giải thích.
Checklist thực tế cho mã đúng, đơn giản và có kỷ luật
Đây là checklist nhẹ “theo Dijkstra” bạn có thể dùng khi review, refactor hoặc trước khi merge. Không phải viết chứng minh cả ngày—mà đặt tính đúng đắn và rõ ràng làm mặc định.
Tự kiểm nhanh (mã mới và refactor)
- Tôi có thể giải thích mã cho đồng đội trong 60 giây không? Nếu cần nhiều “tin tôi đi”, hãy đơn giản hoá.
- Luồng điều khiển có rõ ràng không? Ưu tiên mã thẳng; giữ vòng lặp và điều kiện nhỏ; tránh lối thoát ẩn và lồng quá sâu.
- Preconditions và postconditions là gì? Ghi chúng vào comment, docstring hoặc tên hàm. Nếu không thể nói được, hàm có lẽ làm quá nhiều.
- Mỗi hàm chỉ làm một việc và có ranh giới rõ không? Đầu vào vào, đầu ra ra—ít phụ thuộc trạng thái global.
- Invariant nào giữ vòng lặp này trung thực? Ngay cả một dòng như “
totalluôn bằng tổng các mục đã xử lý” cũng ngăn lỗi tinh vi. - Có ít mẹo “khéo léo” hơn mức cần thiết không? Nếu mã cần người chỉ đường, nó đang tích nợ sự khéo léo.
Những thứ nên đo định tính
- Dễ giải thích: Người không thuộc module có thể nói module làm gì và vì sao đúng không?
- Dễ test: Các trường hợp biên có thể test tự nhiên hay cần mock phức tạp?
- Rủi ro thay đổi: Khi yêu cầu thay đổi, bạn có dự đoán được gì sẽ hỏng không? Nếu mỗi thay đổi đều đáng sợ, ranh giới đang bị rò rỉ.
Bước thực tế tiếp theo
Chọn một module rối và cấu trúc lại luồng điều khiển trước:
- Trích các hàm nhỏ có tên rõ.
- Thay các nhánh rối bằng các trường hợp đơn giản, rõ ràng.
- Đưa các ngoại lệ về cạnh (validate input, early returns).
Sau đó thêm vài test tập trung quanh ranh giới mới. Nếu bạn muốn mẫu tương tự, xem thêm bài liên quan tại /blog.
Câu hỏi thường gặp
Tại sao Dijkstra vẫn quan trọng với các đội phần mềm hiện đại?
Bởi vì khi mã nguồn lớn lên, nút cổ chai chính là hiểu được chứ không phải gõ mã. Việc Dijkstra nhấn mạnh luồng điều khiển có thể dự đoán, hợp đồng rõ ràng và tính đúng đắn làm giảm rủi ro rằng một “thay đổi nhỏ” sẽ gây hành vi bất ngờ ở chỗ khác—điều làm chậm các đội theo thời gian.
“Scale” trong bài này là hiệu năng hay thứ khác?
Trong ngữ cảnh này, “scale” ít liên quan đến hiệu năng hơn và nhiều hơn là sự phức tạp nhân lên:
- nhiều tính năng và các trường hợp biên
- nhiều người đóng góp và chuyển giao
- nhiều tích hợp và chế độ lỗi
- nhiều thời gian và quyết định di sản
Những lực này khiến năng lực suy luận và dự đoán trở nên có giá trị hơn cả sự khéo léo.
Lập trình có cấu trúc là gì, theo cách thực tế?
Lập trình có cấu trúc ưu tiên một tập nhỏ các cấu trúc điều khiển rõ ràng:
- trình tự (làm A rồi B)
- lựa chọn (
if/else,switch) - lặp lại (
for,while)
Mục tiêu không phải là cứng nhắc mà là làm đường đi thực thi dễ theo dõi để bạn có thể giải thích hành vi, rà soát thay đổi và gỡ lỗi mà không phải “dịch chuyển” khắp nơi trong mã.
Tại sao luồng điều khiển “spaghetti” (như `goto` không hạn chế) lại là vấn đề bảo trì?
Vấn đề là nhảy không giới hạn tạo ra các đường đi thực thi khó dự đoán và trạng thái không rõ ràng. Khi luồng điều khiển rối, các lập trình viên tốn thời gian trả lời những câu hỏi cơ bản như “Chúng ta đã đến đây bằng cách nào?” và “Biến này đang ở trạng thái gì bây giờ?”
Các tương đương hiện đại gồm phân nhánh lồng sâu, nhiều lối thoát rải rác và thay đổi trạng thái ẩn khiến hành vi khó lần theo.
Định nghĩa thực tế về tính đúng đắn cho phần mềm sản phẩm là gì?
Tính đúng đắn là “tính năng im lặng” mà người dùng tin tưởng: hệ thống nhất quán thực hiện những gì nó hứa và chỉ thất bại theo cách có thể giải thích khi không thể thực hiện. Đó là khác biệt giữa “nó chạy với vài ví dụ” và “nó tiếp tục chạy sau refactor, tích hợp và xuất hiện các trường hợp biên”.
Tại sao những lỗi nhỏ lại trở nên đắt đỏ trong hệ thống lớn?
Bởi vì phụ thuộc khuếch đại lỗi. Một trạng thái sai nhỏ hoặc lỗi biên độ bị sao chép, lưu cache, retry, bọc trong các service khác và “được vá” rải rác qua nhiều module. Theo thời gian, các đội ngừng hỏi “điều gì là đúng?” và bắt đầu tin “thường thì thế này,” làm các sự cố khó chẩn đoán và thay đổi trở nên rủi ro hơn.
Ở đây “đơn giản” nghĩa là gì (và đâu không phải)?
Đơn giản là ít ý tưởng cùng vận hành cùng lúc: trách nhiệm rõ ràng, luồng dữ liệu rõ ràng và ít trường hợp đặc biệt. Nó không phải là ít dòng mã hay các mẹo một dòng. Một phép thử tốt là khi yêu cầu thay đổi, hành vi vẫn dễ dự đoán — nếu mỗi trường hợp mới thêm vào một “trừ khi…”, bạn đang tích lũy độ phức tạp tình cờ.
Invariant giúp trong mã hàng ngày như thế nào mà không cần chứng minh hình thức?
Một invariant là một điều luôn đúng trong suốt vòng lặp hoặc chuyển trạng thái. Cách dùng nhẹ nhàng:
- viết một comment một dòng phía trên vòng lặp (ví dụ: “
totalbằng tổng các mục đã xử lý”) - chỉnh mã đến khi bạn có thể giữ câu đó đúng ở mỗi bước
- thêm một assertion nếu rẻ và hữu ích
Điều này giúp các chỉnh sửa sau an toàn hơn vì người tiếp theo biết điều gì không được phá vỡ.
Các đội nên cân bằng kiểm thử và suy luận như thế nào?
Kiểm thử tìm lỗi bằng cách chạy ví dụ; suy luận (reasoning) ngăn chặn cả lớp lỗi bằng cách làm logic rõ ràng. Kiểm thử không thể chứng minh không có lỗi vì không thể bao phủ mọi đầu vào hoặc thời điểm. Suy luận đặc biệt giá trị ở các vùng rủi ro cao (tiền, bảo mật, đồng thời).
Hòa hợp thực tế: kiểm thử rộng + các assertion nhắm mục tiêu + preconditions/postconditions rõ ràng quanh logic quan trọng.
Làm sao áp dụng ý tưởng của Dijkstra dần dần mà không giáo điều?
Bắt đầu với các bước nhỏ lặp lại để giảm tải nhận thức:
- trích hàm nhỏ và nêu rõ đầu vào/đầu ra
- thay điều kiện “ma thuật” bằng predicate đặt tên rõ (ví dụ
isEligibleForRefund) - đóng gói chuyển trạng thái phức tạp sau một rào chắn duy nhất
- thêm comment ngắn nêu tại sao mã đúng (invariant, giả định), không phải chỉ mô tả những gì mã đang làm
Đó là các “nâng cấp cấu trúc” từng chút một làm cho lần thay đổi tiếp theo rẻ hơn mà không cần rewrite.