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.

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.
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.
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:
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:
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ó.
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.
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.
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 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.
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ờ” 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:
Tính đúng đắn là xoá các giả định ẩn này—hoặc làm chúng rõ ràng.
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.
Mã rõ ràng cải thiện tính đúng đắn vì nó cải thiện giao tiếp:
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 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.
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:
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.
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.
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 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.
if/else, switch).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.
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.
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.
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.
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.
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:
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”.
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?”).
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ử 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 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 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.
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:
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.
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.
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:
Một vài thói quen cụ thể ngăn nợ sự khéo léo tích tụ:
calculate_total() hơn do_it()).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 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.
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 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:
Đâ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.
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ẹ:
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ộ.
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ư:
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ủ ý.
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.
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.
Đ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.
Bắt đầu với các lựa chọn làm suy luận rẻ:
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.
Bạn không cần sprint refactor lớn. Thêm cấu trúc ở các mối nối:
isEligibleForRefund).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.
Khi review (hoặc viết) thay đổi, hỏi:
Nếu reviewer không trả lời nhanh, mã đang báo hiệu phụ thuộc ẩn.
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).
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:
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.
Đâ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.
total luôn bằng tổng các mục đã xử lý” cũng ngăn lỗi tinh vi.Chọn một module rối và cấu trúc lại luồng điều khiển trước:
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.
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.
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:
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 ưu tiên một tập nhỏ các cấu trúc điều khiển rõ ràng:
if/else, switch)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ã.
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.
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”.
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.
Đơ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ờ.
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:
total bằng tổng các mục đã xử lý”)Đ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ỡ.
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.
Bắt đầu với các bước nhỏ lặp lại để giảm tải nhận thức:
isEligibleForRefund)Đó 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.