KoderKoder.ai
Bảng giáDoanh nghiệpGiáo dụcDành cho nhà đầu tư
Đăng nhậpBắt đầu

Sản phẩm

Bảng giáDoanh nghiệpDành cho nhà đầu tư

Tài nguyên

Liên hệHỗ trợGiáo dụcBlog

Pháp lý

Chính sách bảo mậtĐiều khoản sử dụngBảo mậtChính sách sử dụng chấp nhận đượcBáo cáo vi phạm

Mạng xã hội

LinkedInTwitter
Koder.ai
Ngôn ngữ

© 2026 Koder.ai. Bảo lưu mọi quyền.

Trang chủ›Blog›Rich Hickey & Clojure: Đơn giản, Bất biến, Mặc định tốt hơn
27 thg 6, 2025·8 phút

Rich Hickey & Clojure: Đơn giản, Bất biến, Mặc định tốt hơn

Cái nhìn dễ tiếp cận về tư tưởng của Rich Hickey: đơn giản, bất biến và các mặc định tốt hơn—bài học thực tế để xây hệ thống phức tạp bình tĩnh và an toàn hơn.

Rich Hickey & Clojure: Đơn giản, Bất biến, Mặc định tốt hơn

Tại sao độ phức tạp vẫn “thắng” trong các dự án thực tế

Phần mềm hiếm khi trở nên phức tạp ngay lập tức. Nó đến dần qua từng quyết định “hợp lý”: một cache nhanh để kịp deadline, một đối tượng mutable chia sẻ để tránh sao chép, một ngoại lệ vì “cái này đặc biệt”. Mỗi lựa chọn trông nhỏ, nhưng khi ghép lại chúng tạo ra hệ thống nơi thay đổi trở nên rủi ro, bug khó tái tạo, và việc thêm tính năng mất nhiều thời gian hơn xây nó.

Độ phức tạp thắng vì nó mang lại sự thoải mái ngắn hạn. Thường nhanh hơn khi thêm một dependency mới hơn là đơn giản hóa một cái đã có. Dễ hơn khi vá trạng thái hơn là hỏi tại sao trạng thái được phân tán qua năm dịch vụ. Và người ta thường dựa vào quy ước và kiến thức nội bộ khi hệ thống lớn nhanh hơn tài liệu.

Bài viết này là gì (và không phải là gì)

Đây không phải là tutorial Clojure, và bạn không cần biết Clojure để nhận giá trị từ nó. Mục tiêu là mượn một loạt ý tưởng thực tế thường gắn với tác phẩm của Rich Hickey—những ý tưởng bạn có thể áp dụng cho quyết định kỹ thuật hàng ngày, bất kể ngôn ngữ.

Tại sao mặc định quan trọng hơn bạn nghĩ

Phần lớn độ phức tạp không đến từ code bạn viết có chủ đích; nó đến từ những gì công cụ làm dễ theo mặc định. Nếu mặc định là “đối tượng mutable ở khắp nơi”, bạn sẽ kết thúc với coupling ẩn. Nếu mặc định là “trạng thái sống trong bộ nhớ”, bạn sẽ gặp khó khi debug và truy vết. Mặc định hình thành thói quen, và thói quen hình thành hệ thống.

Chúng ta sẽ tập trung vào ba chủ đề:

  • Đơn giản: không phải ít tính năng hơn, mà là ít phần chuyển động hơn và ít trường hợp đặc biệt hơn.
  • Bất biến: coi dữ liệu như các giá trị không thay đổi, để bạn có thể lý giải chúng.
  • Mặc định tốt hơn: chọn các mẫu khiến phương án an toàn, dự đoán được, là phương án dễ nhất.

Những ý tưởng này không loại bỏ độ phức tạp khỏi miền bài toán, nhưng chúng có thể ngăn phần mềm của bạn nhân lên độ phức tạp đó.

Rich Hickey và điều Clojure muốn khắc phục

Rich Hickey là một lập trình viên và nhà thiết kế phần mềm lâu năm, nổi tiếng vì tạo ra Clojure và những bài nói thách thức các thói quen lập trình phổ biến. Ông không chạy theo xu hướng—mà chú tâm đến những lý do lặp lại khiến hệ thống khó thay đổi, khó suy nghĩ, và khó tin cậy khi chúng lớn lên.

Clojure là gì (ở mức cao, không thuật ngữ)

Clojure là một ngôn ngữ hiện đại chạy trên các nền tảng phổ biến như JVM (runtime của Java) và JavaScript. Nó được thiết kế để làm việc với hệ sinh thái sẵn có trong khi khuyến khích một phong cách cụ thể: biểu diễn thông tin như dữ liệu thuần, ưu tiên giá trị không thay đổi, và tách rời “những gì đã xảy ra” khỏi “những gì hiển thị trên màn hình”.

Bạn có thể nghĩ về nó như một ngôn ngữ khuyến khích bạn dùng các khối xây dựng rõ ràng hơn và tránh các hiệu ứng phụ ẩn.

Những vấn đề nó muốn giảm thiểu

Clojure không được tạo ra để làm các script nhỏ ngắn hơn. Nó nhắm đến những nỗi đau lặp lại trong dự án:

  • Độ phức tạp tăng do trạng thái chia sẻ: khi nhiều phần hệ thống có thể sửa cùng một dữ liệu, bug trở nên phụ thuộc vào thời điểm và khó tái tạo.
  • Coupling chặt giữa dữ liệu và hành vi: khi thông tin bị khóa trong object hoặc class, việc tái sử dụng khó hơn và thay đổi gây chấn động khắp codebase.
  • Đau đầu đồng thời: khi hệ thống thêm job nền, queue và công việc song song, câu hỏi “ai thay đổi gì, khi nào?” trở thành vấn đề hàng ngày.

Mặc định của Clojure đẩy theo hướng ít bộ phận chuyển động hơn: cấu trúc dữ liệu ổn định, cập nhật rõ ràng, và công cụ làm cho việc phối hợp an toàn hơn.

Hữu ích ngay cả khi bạn không dùng Clojure

Giá trị không chỉ nằm ở việc đổi ngôn ngữ. Những ý tưởng cốt lõi của Hickey—đơn giản hóa bằng cách loại bỏ phụ thuộc không cần thiết, coi dữ liệu như các sự thật bền vững, và giảm thiểu trạng thái mutable—có thể cải thiện hệ thống trong Java, Python, JavaScript và hơn thế nữa.

Đơn giản: không phải “dễ”, mà là ít phần chuyển động hơn

Rich Hickey phân biệt rõ ràng giữa đơn giản và dễ—và đó là ranh giới nhiều dự án đi qua mà không nhận ra.

Đơn giản vs. dễ (với ví dụ đời thường)

Dễ là về cảm nhận ngay lúc đó. Đơn giản là về có bao nhiêu phần và chúng bị rối với nhau đến mức nào.

  • Mì ăn liền thì dễ. Một món hầm cơ bản có thể là đơn giản: vài thành phần, một nồi, không có gì ẩn giấu.
  • Một điều khiển từ xa với 60 nút có thể làm một tính năng “dễ” (nó ở đó), nhưng nó không đơn giản. Một điều khiển 6 nút rõ ràng thì đơn giản hơn, dù mất một phút để học.

Trong phần mềm, “dễ” thường nghĩa là “nhanh để gõ hôm nay”, trong khi “đơn giản” nghĩa là “khó vỡ vào tháng tới”.

Làm thế nào “dễ ngay” tạo độ phức tạp cho tương lai

Nhóm thường chọn lối tắt giảm ma sát tức thì nhưng thêm cấu trúc vô hình phải bảo trì:

  • “Thêm một flag thôi.” Giờ mọi tính năng phải xét flag đó.
  • “Ta sẽ lưu giá trị tính toán để tiết kiệm thời gian.” Giờ bạn phải giữ nó đồng bộ qua các luồng mã.
  • “Ta vá ở UI.” Giờ cùng một quy tắc nghiệp vụ tồn tại ở nhiều nơi.

Mỗi lựa chọn có thể trông nhanh, nhưng nó tăng số phần chuyển động, trường hợp đặc biệt, và phụ thuộc chéo. Đó là cách hệ thống trở nên mong manh mà không có một sai lầm lớn duy nhất.

Tốc độ không giống với tính đơn giản

Ra tính năng nhanh có thể tốt—nhưng tốc độ mà không đơn giản hóa thường nghĩa là bạn đang vay nợ cho tương lai. Lãi suất xuất hiện dưới dạng bug khó tái tạo, onboarding kéo dài, và thay đổi đòi hỏi “phối hợp cẩn thận”.

Một checklist nhanh cho độ phức tạp vô tình

Hỏi các câu này khi review thiết kế hoặc PR:

  • Chúng ta có giới thiệu chế độ, flag hay nhánh cấu hình mới không?
  • Chúng ta có cache hoặc sao chép dữ liệu cần giữ nhất quán không?
  • Có nhiều module cần thay đổi cùng nhau vì một hành vi không?
  • Quy tắc được triển khai ở hơn một nơi không?
  • Một người mới có dự đoán được cách hoạt động mà không cần giải thích thêm không?

Trạng thái: nhân tố vô hình làm tăng độ phức tạp

“Trạng thái” đơn giản là những thứ trong hệ thống có thể thay đổi: giỏ hàng người dùng, số dư tài khoản, cấu hình hiện tại, bước đang thực hiện trong workflow. Điểm khó là không phải thay đổi tồn tại—mà là mỗi thay đổi tạo cơ hội cho mọi thứ không khớp.

Khi người ta nói “trạng thái gây lỗi”, họ thường có ý: nếu cùng một thông tin có thể khác nhau theo thời điểm (hoặc nơi), mã của bạn phải liên tục trả lời: “Phiên bản nào là thật ngay bây giờ?” Trả lời sai gây lỗi thấy như ngẫu nhiên.

Mutability: thay đổi bạn không thể quay lại mắt

Mutability nghĩa là một đối tượng bị sửa tại chỗ: “cùng” thứ trở nên khác theo thời gian. Nghe có vẻ hiệu quả, nhưng làm cho việc lập luận khó hơn vì bạn không thể tin những gì đã thấy một lát trước.

Ví dụ dễ hiểu là một bảng tính chia sẻ. Nếu nhiều người có thể sửa cùng ô cùng lúc, hiểu biết của bạn bị vô hiệu ngay: tổng đổi, công thức hỏng, hoặc một hàng biến mất vì ai đó tổ chức lại. Dù không ai cố ý, tính “chia sẻ, có thể chỉnh sửa” là nguồn nhầm lẫn.

Trạng thái phần mềm hành xử tương tự. Nếu hai phần của hệ thống đọc cùng một giá trị mutable, một phần có thể im lặng thay đổi nó trong khi phần kia tiếp tục với giả định lỗi thời.

Tại sao debug trở nên đau đớn

Trạng thái mutable biến việc debug thành khảo cổ. Báo cáo lỗi hiếm khi nói “dữ liệu bị thay đổi sai lúc 10:14:03.” Bạn chỉ thấy kết quả cuối: số sai, trạng thái bất ngờ, request đôi khi thất bại.

Vì trạng thái thay đổi theo thời gian, câu hỏi quan trọng nhất trở thành: chuỗi chỉnh sửa nào dẫn đến đây? Nếu bạn không thể dựng lại lịch sử đó, hành vi trở nên không thể đoán:

  • Cùng một hành động cho kết quả khác tùy thời điểm.
  • Fix “chạy trên máy tôi” nhưng không trên production.
  • Thêm logging thay đổi thời điểm và lỗi biến mất.

Đây là lý do Hickey coi trạng thái như nhân tố tăng: khi dữ liệu vừa được chia sẻ và mutable, số các tương tác có thể xảy ra tăng nhanh hơn khả năng bạn theo dõi chúng.

Bất biến giải thích mà không dùng thuật ngữ khoa học máy tính

Bất biến đơn giản nghĩa là dữ liệu không đổi sau khi được tạo. Thay vì chỉnh sửa một thông tin tồn tại tại chỗ, bạn tạo ra một thông tin mới phản ánh cập nhật.

Hãy nghĩ về một hóa đơn: khi in ra, bạn không xóa dòng rồi ghi lại tổng. Nếu có thay đổi, bạn phát hành hóa đơn điều chỉnh. Hóa đơn cũ vẫn tồn tại, và hóa đơn mới rõ ràng là “phiên bản mới nhất”.

Tại sao nó giảm bất ngờ

Khi dữ liệu không thể bị sửa lén, bạn không phải lo lắng về việc có những chỉnh sửa vô hình. Điều đó làm cho việc lý luận hàng ngày dễ hơn nhiều:

  • Nếu bạn giữ một giá trị, bạn có thể tin nó sẽ giữ nguyên.
  • Bug dễ tái tạo hơn vì cùng đầu vào cho cùng hành vi.
  • Chia sẻ dữ liệu giữa các phần an toàn hơn vì không ai vô tình “phá” nó cho người khác.

Đây là phần lớn lý do Hickey nói về đơn giản: ít hiệu ứng phụ ẩn nghĩa là ít nhánh tinh thần phải theo dõi.

“Phiên bản mới” vs. “sửa tại chỗ”

Tạo phiên bản mới nghe có vẻ tốn, cho đến khi bạn so sánh với phương án khác. Sửa tại chỗ khiến bạn phải hỏi: “Ai đã thay đổi? Khi nào? Trước đó ra sao?” Với dữ liệu bất biến, thay đổi rõ ràng: một phiên bản mới tồn tại và phiên bản cũ vẫn có để debug, audit, hoặc rollback.

Clojure ủng hộ điều này bằng cách làm cho việc coi cập nhật như tạo giá trị mới trở nên tự nhiên.

Các đánh đổi cần thẳng thắn

Bất biến không miễn phí. Bạn có thể tạo nhiều đối tượng hơn, và nhóm quen với “chỉ cập nhật cái đó” có thể cần thời gian thích nghi. Tin tốt là các thực thi hiện đại thường chia sẻ cấu trúc ở mức độ dưới nắp để giảm chi phí bộ nhớ, và lợi ích thường là hệ thống bình tĩnh hơn với ít sự cố khó giải thích.

Đồng thời dễ hơn khi dữ liệu không thay đổi

Giữ logic di động sạch
Khởi động một app di động Flutter từ chat và giữ logic nghiệp vụ rõ ràng, dễ kiểm thử.
Xây Mobile

Concurrency chỉ là “nhiều thứ xảy ra cùng lúc”. Một app web xử lý hàng ngàn request, một hệ thống thanh toán cập nhật số dư trong khi sinh hóa đơn, hay app di động đồng bộ nền—tất cả đều là concurrency.

Điểm khó không phải nhiều thứ cùng xảy ra, mà là chúng thường chạm vào cùng dữ liệu.

Tại sao dữ liệu chia sẻ, có thể thay đổi tạo race condition

Khi hai worker đều đọc rồi sửa cùng một giá trị, kết quả cuối cùng có thể phụ thuộc vào thời điểm. Đó là race condition: lỗi khó tái tạo, xuất hiện khi hệ thống bận.

Ví dụ: hai request cố cập nhật tổng đơn hàng.

  1. Request A đọc total = 100
  2. Request B đọc total = 100
  3. A thêm 20 và ghi 120
  4. B thêm 10 và ghi 110

Không có gì “crash”, nhưng bạn mất cập nhật. Khi tải tăng, những cửa sổ thời gian này phổ biến hơn.

Cách truyền thống—khóa, synchronized—hoạt động, nhưng buộc mọi người phải phối hợp. Phối hợp tốn kém: làm chậm throughput và trở nên mong manh khi codebase lớn.

Bất biến giảm thiểu nhu cầu phối hợp

Với dữ liệu bất biến, một giá trị không bị sửa tại chỗ. Thay vào đó bạn tạo giá trị mới biểu diễn thay đổi.

Sự thay đổi nhỏ này loại bỏ cả một loại vấn đề:

  • Reader không lo dữ liệu họ đang xem bị thay đổi giữa chừng.
  • Writer không “đấu” trên cùng vùng nhớ; họ tạo phiên bản mới.
  • Hệ thống có thể chọn cách an toàn để công bố phiên bản mới nhất (thường với primitive đơn giản, đã kiểm thử).

Kết quả: hành vi dự đoán được khi tải cao

Bất biến không làm concurrency miễn phí—bạn vẫn cần quy tắc về phiên bản nào là hiện tại. Nhưng nó làm các chương trình đồng thời trở nên dễ dự đoán hơn, vì bản thân dữ liệu không còn là mục tiêu chuyển động. Khi lưu lượng tăng hoặc job nền dồn đống, bạn ít gặp lỗi phụ thuộc thời điểm bí ẩn.

“Mặc định tốt hơn” có nghĩa là gì trong thực tế

“Mặc định tốt hơn” nghĩa là lựa chọn an toàn xảy ra tự động, và bạn chỉ chịu rủi ro thêm khi bạn chủ động tắt nó đi.

Nghe có vẻ nhỏ, nhưng mặc định hướng dẫn người ta viết gì vào một sáng thứ Hai, người review chấp nhận gì vào chiều thứ Sáu, và người mới học được gì từ codebase đầu tiên họ chạm.

Các mặc định giảm rủi ro

“Mặc định tốt hơn” không phải quyết định hộ bạn mọi thứ. Nó là khiến con đường phổ biến ít lỗi hơn.

Ví dụ:

  • Dữ liệu bất biến làm mặc định: thay vì “sửa cái đó”, bạn tạo phiên bản mới. Điều này khiến việc vô tình ảnh hưởng phần khác của chương trình khó xảy ra.
  • Hàm thuần (pure) là phong cách bình thường: hàm nhận input và trả output, không thay đổi dữ liệu chia sẻ hay phụ thuộc vào trạng thái toàn cục ẩn. Điều này làm hành vi dễ dự đoán và kiểm thử.
  • Thay đổi trạng thái rõ ràng: khi phải thay đổi, nó xảy ra qua cơ chế rõ ràng, định nghĩa tốt (thay vì bất kỳ phần nào của code có thể mutate bất cứ thứ gì).

Không thứ nào trong số này loại bỏ độ phức tạp, nhưng chúng ngăn nó lan rộng.

Mặc định định hình nhóm và review

Nhóm không chỉ theo tài liệu—they theo cách code “muốn” bạn làm.

Khi mutate trạng thái chia sẻ dễ, nó trở thành lối tắt bình thường, và reviewer phải tranh luận về ý định: “Điều này an toàn ở đây không?” Khi bất biến và hàm thuần là mặc định, reviewer có thể tập trung vào logic và tính đúng đắn, vì các động thái rủi ro nổi bật.

Nói cách khác, mặc định tốt tạo chuẩn cơ bản khỏe mạnh: hầu hết thay đổi có vẻ nhất quán, và mẫu bất thường đủ rõ để bị đặt câu hỏi.

Bảo trì và onboarding

Bảo trì lâu dài chủ yếu là đọc và thay đổi code hiện có một cách an toàn.

Mặc định tốt giúp người mới lên tay vì ít quy tắc ẩn (“cẩn thận, hàm này bí mật cập nhật cái bản đồ global kia”). Hệ thống dễ lý giải hơn, giảm chi phí cho mọi tính năng, sửa lỗi, và refactor tương lai.

Tách facts khỏi views: thời gian, lịch sử và khả năng truy vết

Một chuyển đổi tư duy hữu ích trong các bài nói của Hickey là tách sự thật (điều đã xảy ra) khỏi view (những gì ta hiện tin là đúng). Hầu hết hệ thống trộn chúng bằng cách chỉ lưu giá trị mới nhất—ghi đè hôm qua bằng hôm nay—và điều đó làm thời gian biến mất.

Facts là append-only; views thì được dẫn xuất

Một fact là bản ghi bất biến: “Đơn #4821 được đặt lúc 10:14”, “Thanh toán thành công”, “Địa chỉ thay đổi”. Những thứ này không bị sửa; bạn thêm fact mới khi thực tế thay đổi.

Một view là những gì app cần bây giờ: “Địa chỉ giao hàng hiện tại là gì?” hoặc “Số dư khách hàng là bao nhiêu?” Views có thể được tính lại từ facts, cache, index hoặc materialize để tăng tốc.

Tại sao giữ lịch sử có lợi

Khi bạn giữ facts, bạn có:

  • Khả năng kiểm toán: bạn có thể giải thích tại sao giá trị hiện tại là như vậy.
  • Gỡ lỗi: bạn có thể phát lại chuỗi sự kiện và tìm thời điểm mọi thứ lệch.
  • Truy vết: “Ai thay đổi cái này, khi nào, trước đó ra sao?” trở thành câu hỏi dữ liệu, không phải câu chuyện thám tử.

Ví dụ dễ hiểu: cập nhật đè lên vs ghi thêm

Ghi đè bản ghi giống như cập nhật ô bảng tính: bạn chỉ thấy số mới nhất.

Một log append-only giống sổ ghi séc: mỗi mục là một fact, và “số dư hiện tại” là view tính từ các mục.

Không phải hệ thống nào cũng cần event sourcing đầy đủ

Bạn không cần chuyển sang kiến trúc event-sourced đầy đủ để hưởng lợi. Nhiều nhóm bắt đầu nhỏ: giữ bảng audit append-only cho thay đổi quan trọng, lưu các “sự kiện thay đổi” cho một vài workflow rủi ro cao, hoặc giữ snapshot cộng với cửa sổ lịch sử ngắn. Cốt lõi là thói quen: coi facts như bền vững, và trạng thái hiện tại như projection tiện lợi.

Dữ liệu trước tiên: làm thông tin bền và linh hoạt

Xây lõi ứng dụng nhanh
Tạo nhanh ứng dụng web React với backend Go và PostgreSQL mà không phải nối mọi thứ bằng tay.
Tạo app

Một trong những ý tưởng thực tế nhất của Hickey là data-first: coi thông tin hệ thống như giá trị thuần (facts), và coi hành vi là thứ bạn chạy trên những giá trị đó.

Dữ liệu bền. Nếu bạn lưu thông tin rõ ràng, tự chứa, bạn có thể diễn giải lại sau, chuyển giữa dịch vụ, reindex, audit, hoặc cấp cho tính năng mới. Hành vi kém bền hơn—code thay đổi, giả định thay đổi, dependency thay đổi.

Giá trị vs hành động (không thuật ngữ)

  • Dữ liệu (giá trị): “Cái gì là đúng?” Email khách, tổng đơn, timestamp, trạng thái.
  • Hành vi (hành động): “Chúng ta làm gì?” Validate, tính giảm giá, gửi thông báo, quyết định ý nghĩa của trạng thái.

Khi bạn trộn chúng, hệ thống trở nên dính: bạn không thể tái dùng dữ liệu mà không kéo theo hành vi đã tạo nó.

Ít coupling, nhiều tái sử dụng hơn

Tách facts khỏi actions giảm coupling vì các thành phần có thể đồng thuận về hình dạng dữ liệu mà không cần đồng thuận về đường dẫn code dùng chung.

Một job báo cáo, công cụ hỗ trợ, và dịch vụ thanh toán có thể tiêu thụ cùng dữ liệu đơn hàng, mỗi cái áp logic riêng. Nếu bạn nhúng logic trong đại diện lưu trữ, mọi consumer trở nên phụ thuộc vào logic đó—và thay đổi nó trở thành rủi ro.

Ví dụ: lưu dữ liệu sạch vs lưu “tiểu chương trình”

Dữ liệu sạch (dễ tiến hóa):

{
  "type": "discount",
  "code": "WELCOME10",
  "percent": 10,
  "valid_until": "2026-01-31"
}

Tiểu-chương-trình trong storage (khó tiến hóa):

{
  "type": "discount",
  "rule": "if (customer.orders == 0) return total * 0.9; else return total;"
}

Phiên bản thứ hai trông linh hoạt, nhưng đẩy độ phức tạp vào layer dữ liệu: bạn cần một evaluator an toàn, quy tắc versioning, rào bảo mật, công cụ debug, và kế hoạch migration khi ngôn ngữ quy tắc thay đổi.

Tại sao điều này làm hệ thống dễ tiến hóa

Khi thông tin lưu giữ đơn giản và rõ ràng, bạn có thể thay đổi hành vi theo thời gian mà không viết lại lịch sử. Bản ghi cũ vẫn đọc được. Dịch vụ mới có thể được thêm vào mà không phải “hiểu” quy tắc thực thi cũ. Và bạn có thể giới thiệu cách diễn giải mới—giao diện UI mới, chiến lược giá mới, phân tích mới—bằng cách viết code mới, không bằng cách mutate ý nghĩa dữ liệu.

Áp dụng ý tưởng cho hệ thống phức tạp (không cần rewrite)

Hầu hết hệ thống doanh nghiệp không hỏng vì một module “xấu”. Chúng hỏng vì mọi thứ liên kết với nhau.

Các chế độ hỏng cần chú ý

Coupling chặt xuất hiện như thay đổi “nhỏ” nhưng yêu cầu test lại cả tuần. Một trường mới thêm vào service này phá ba consumer downstream. Schema DB chia sẻ trở thành nút thắt phối hợp. Một cache mutable hoặc đối tượng singleton “config” lén trở thành dependency của nửa codebase.

Thay đổi dây chuyền là kết quả tự nhiên: khi nhiều phần cùng chia sẻ một thứ thay đổi, bán kính ảnh hưởng mở rộng. Nhóm phản ứng bằng cách thêm quy trình, thêm quy tắc, và nhiều bàn giao—thường làm chậm việc giao hàng hơn.

Giảm bán kính ảnh hưởng bằng ranh giới đơn giản hơn

Bạn có thể áp dụng ý tưởng của Hickey mà không đổi ngôn ngữ hay viết lại toàn bộ:

  • Ưu tiên dữ liệu bất biến ở ranh giới. Xử lý message, event, và input API như facts không được sửa tại chỗ. Nếu có thay đổi, tạo phiên bản mới.
  • Di chuyển trạng thái ra mép. Giữ logic lõi là các biến đổi thuần: dữ liệu vào → dữ liệu ra. Để database, cache, UI xử lý “trạng thái hiện tại”.
  • Ngừng chia sẻ cấu trúc mutable. Nếu hai module cùng ghi vào cùng object, chúng bị coupling. Truyền giá trị, không truyền reference bạn định mutate.

Khi dữ liệu không bị thay đổi dưới chân bạn, bạn tốn ít thời gian debug “nó vào trạng thái này bằng cách nào?” và nhiều thời gian để lý luận code làm gì.

“Mặc định tốt” trên toàn nhóm

Mặc định là nơi bất nhất lẻn vào: mỗi nhóm tự tạo format timestamp riêng, shape lỗi riêng, chính sách retry riêng, và cách tiếp cận concurrency riêng.

Mặc định tốt trông như: schema sự kiện versioned, DTO bất biến chuẩn, quyền sở hữu ghi rõ ràng, và một tập thư viện được chấp nhận cho serialization, validation, và tracing. Kết quả là ít tích hợp bất ngờ và ít fix một-off.

Áp dụng dần dần hiệu quả trong codebase hiện có

Bắt đầu nơi thay đổi đã diễn ra:

  1. Bọc module hiện tại bằng API nhận/trả dữ liệu bất biến.
  2. Chuyển một workflow churn cao sang kiểu event “append-only”.
  3. Thay cache mutable chia sẻ bằng view có thể tái tính từ facts bền.

Cách này cải thiện độ tin cậy và phối hợp nhóm trong khi giữ hệ thống chạy—và giữ scope nhỏ đủ để hoàn thành.

Nơi nền tảng và công cụ có thể củng cố “mặc định tốt hơn”

Nguyên mẫu con đường đơn giản hơn
Biến ý tưởng refactor tiếp theo thành prototype hoạt động bằng cách chat để tạo nó.
Xây mẫu

Dễ áp dụng những ý tưởng này hơn khi workflow của bạn hỗ trợ lặp nhanh, rủi ro thấp. Ví dụ, nếu bạn xây tính năng mới trong Koder.ai (một nền tảng vibe-coding dựa trên chat cho web, backend, và mobile), hai tính năng map trực tiếp tới tư duy “mặc định tốt hơn”:

  • Chế độ Planning khuyến khích bạn làm rõ ranh giới và hình dạng dữ liệu trước khi triển khai—thường là khác biệt giữa luồng dữ liệu đơn giản và coupling vô tình.
  • Snapshots và rollback khiến thay đổi an toàn hơn để ship, vì bạn có thể nhanh chóng quay lại khi một lối tắt “dễ” biến thành cú spike phức tạp.

Dù stack bạn là React + Go + PostgreSQL (hoặc Flutter cho mobile), điểm cốt lõi vẫn vậy: công cụ bạn dùng hàng ngày dạy một cách làm việc mặc định. Chọn công cụ khiến truy vết, rollback và lập kế hoạch rõ ràng trở thành thường quy có thể giảm áp lực “chỉ vá tạm” ở phút chót.

Các đánh tradeoff, giới hạn, và tránh chủ nghĩa

Đơn giản và bất biến là mặc định mạnh mẽ, không phải quy tắc đạo đức. Chúng giảm số thứ có thể bất ngờ thay đổi, giúp khi hệ thống lớn. Nhưng dự án thực có ngân sách, deadline và ràng buộc—và đôi khi mutable là công cụ đúng.

Khi mutability chấp nhận được

Mutability có thể là lựa chọn thực tế ở điểm nóng hiệu năng (vòng lặp chặt, parsing throughput cao, đồ họa, tính toán số). Nó cũng ổn khi phạm vi được kiểm soát: biến cục bộ, cache riêng tư ẩn sau interface, hoặc component đơn luồng với ranh giới rõ ràng.

Quy tắc then chốt là cô lập. Nếu “đối tượng mutable” không bao giờ lọt ra, nó không thể lan rộng độ phức tạp sang codebase.

Giới hạn bằng quyền sở hữu và interface

Ngay cả trong phong cách chủ yếu là hàm, nhóm vẫn cần quyền sở hữu rõ ràng:

  • Một module sở hữu mảnh trạng thái và phơi ra API nhỏ.
  • Dữ liệu vượt ranh giới dưới dạng giá trị thuần (không phải object sống với hành vi bí mật).
  • Hiệu ứng phụ đẩy ra mép (I/O, DB, thời gian).

Đây là nơi thiên kiến của Clojure về dữ liệu và ranh giới rõ ràng hữu dụng, nhưng kỷ luật là kiến trúc hơn là ngôn ngữ.

Clojure không giải quyết được gì

Không ngôn ngữ nào sửa được yêu cầu kém, mô hình miền mơ hồ, hoặc team không thống nhất khái niệm “xong”. Bất biến không làm workflow khó hiểu trở nên rõ ràng, và code “functional” vẫn có thể mã hóa sai luật nghiệp vụ—chỉ là trình bày gọn hơn.

Tránh giáo điều: giảm rủi ro bằng thay đổi nhỏ nhất

Nếu hệ thống đã production, đừng xem những ý tưởng này như rewrite tất-cả-hay-không. Tìm bước nhỏ nhất giảm rủi ro:

  • Thay cấu trúc mutable chia sẻ bằng dữ liệu bất biến ở ranh giới module.
  • Thêm logs sự kiện append-only hoặc lịch sử nơi audit quan trọng.
  • Bọc trạng thái legacy sau interface hẹp và ngăn không cho nó lan rộng.

Mục tiêu không phải là tinh khiết—mà là ít bất ngờ hơn cho mỗi thay đổi.

Checklist thực tế để tiến tới phần mềm đơn giản hơn

Đây là checklist cỡ sprint bạn có thể áp dụng mà không đổi ngôn ngữ, framework, hay cấu trúc nhóm.

3–5 việc thử trong sprint tới

  1. Làm hình dạng dữ liệu mặc định bất biến. Xử lý request/response, event, message như giá trị tạo xong không sửa. Nếu phải thay đổi, tạo phiên bản mới.
  2. Ưu tiên hàm thuần ở giữa workflow. Chọn một workflow (ví dụ: pricing, permissions, checkout) và refactor lõi thành hàm nhận dữ liệu vào và trả dữ liệu ra—không đọc/ghi ẩn.
  3. Di chuyển trạng thái đến ít nơi rõ ràng hơn. Chọn một nguồn sự thật cho mỗi khái niệm (tình trạng khách hàng, feature flag, tồn kho). Nếu nhiều module giữ bản copy riêng, làm cho đó là quyết định rõ ràng với chiến lược sync.
  4. Thêm log append-only cho facts chính. Với một miền, ghi “những gì đã xảy ra” như sự kiện bền (dù bạn vẫn lưu trạng thái hiện tại). Điều này cải thiện truy vết và giảm phỏng đoán.
  5. Định nghĩa mặc định an toàn trong API. Mặc định nên giảm hành vi bất ngờ: timezone rõ ràng, xử lý null rõ ràng, retry rõ ràng, đảm bảo ordering rõ ràng.

Câu hỏi để review thiết kế (dùng nguyên văn)

  • What are the mutable parts here, and who is allowed to change them?
  • Can we model this as “facts” plus a derived view instead of overwriting fields?
  • If two requests run at the same time, what breaks?
  • Which defaults are we relying on (time, ordering, retries, caching), and are they documented?
  • What would we need to debug this three months from now?

Chủ đề gợi ý nên xem lại từ các bài nói của Hickey

Tìm tài liệu về sự khác biệt giữa simplicity và ease, quản lý trạng thái, thiết kế hướng giá trị, bất biến, và cách “lịch sử” (facts theo thời gian) giúp gỡ lỗi và vận hành.

Đơn giản không phải là tính năng dán vào—nó là chiến lược bạn thực hành qua những lựa chọn nhỏ, lặp lại.

Câu hỏi thường gặp

Why does complexity keep “winning” in real projects?

Độ phức tạp tích tụ qua những quyết định nhỏ, hợp lý lúc đó (cờ phụ, cache, ngoại lệ, hàm trợ giúp dùng chung) tạo ra chế độ và sự phụ thuộc.

Một tín hiệu rõ ràng là khi một “thay đổi nhỏ” đòi hỏi chỉnh sửa phối hợp trên nhiều module hoặc service, hoặc khi người review phải dựa vào kiến thức nội bộ để đánh giá tính an toàn.

What’s the difference between “simple” and “easy” in software?

Bởi vì các lối tắt tối ưu cho ma sát hôm nay (thời gian ra mắt) trong khi đẩy chi phí sang tương lai: thời gian gỡ lỗi, chi phí phối hợp, và rủi ro khi thay đổi.

Một thói quen hữu ích là hỏi trong thiết kế/PR: “Việc này tạo ra những bộ phận chuyển động hay trường hợp đặc biệt nào mới, và ai sẽ duy trì chúng?”

How do programming language and framework defaults create accidental complexity?

Các mặc định định hình hành vi của kỹ sư khi áp lực. Nếu mutable là mặc định, trạng thái chia sẻ sẽ lan rộng. Nếu “lưu trong bộ nhớ là đủ” là mặc định, khả năng truy vết sẽ biến mất.

Cải thiện mặc định bằng cách khiến con đường an toàn là con đường ít kháng cự nhất: dữ liệu bất biến ở ranh giới, timezone/null/retry rõ ràng, và quyền sở hữu trạng thái được xác định.

Why is state described as a “complexity multiplier”?

Trạng thái là bất cứ thứ gì thay đổi theo thời gian. Vấn đề là mỗi thay đổi tạo cơ hội xảy ra bất đồng: hai thành phần có thể giữ các giá trị “hiện tại” khác nhau.

Lỗi xuất hiện dưới dạng hành vi phụ thuộc thời gian ("chạy trên máy tôi được", lỗi lặt vặt ở production) vì câu hỏi trở thành: chúng ta đã hành động trên phiên bản dữ liệu nào?

What does immutability mean in practical, non-academic terms?

Bất biến nghĩa là bạn không sửa một giá trị tại chỗ; thay vào đó bạn tạo giá trị mới mô tả cập nhật.

Thực tế, điều này giúp:

  • Người đọc có thể tin dữ liệu sẽ không thay đổi trong lúc sử dụng.
  • Việc tái tạo lỗi dễ hơn (đầu vào ổn định).
  • Chia sẻ dữ liệu giữa luồng/module an toàn hơn.
When is mutability acceptable (or even preferable)?

Không phải lúc nào cũng. Mutability có thể là lựa chọn tốt khi nó được cô lập:

  • Biến cục bộ trong hàm
  • Điểm nóng hiệu năng (vòng lặp chặt, parsing, tính toán số)
  • Cache riêng tư phía sau một interface hẹp

Quy tắc then chốt: đừng để cấu trúc mutable lọt ra ngoài biên giới mà nhiều phần có thể đọc/ghi.

How does immutability help with concurrency under load?

Các điều kiện chạy đua thường đến từ dữ liệu chia sẻ, có thể thay đổi mà nhiều bên đọc rồi ghi.

Bất biến giảm bề mặt cần phối hợp vì writer tạo các phiên bản mới thay vì sửa cùng một đối tượng. Bạn vẫn cần cơ chế để công bố phiên bản hiện tại, nhưng dữ liệu tự nó không còn là mục tiêu di chuyển.

What does it mean to separate “facts” from “views,” and how can I apply it incrementally?

Đối xử với facts như bản ghi append-only của những gì đã xảy ra, và dùng view làm dự kiến trạng thái hiện tại từ những facts đó.

Bắt đầu nhỏ mà không cần event sourcing đầy đủ:

  • Thêm bảng audit cho các thay đổi quan trọng
  • Ghi sự kiện thay đổi cho một workflow rủi ro cao
  • Giữ snapshot cùng cửa sổ lịch sử giới hạn để replay/gỡ lỗi
What is “data-first” design and why does it reduce coupling?

Lưu thông tin dưới dạng dữ liệu rõ ràng, thuần túy (giá trị), và chạy hành vi trên chúng. Tránh nhúng quy tắc thực thi bên trong bản ghi lưu trữ.

Điều này làm hệ thống dễ tiến hóa vì:

  • Bản ghi cũ vẫn đọc được khi code thay đổi
  • Người tiêu thụ mới có thể tái dùng cùng hình dạng dữ liệu
  • Bạn có thể thay đổi logic mà không phải viết lại lịch sử
What are the first 3 concrete changes to try next sprint to reduce complexity?

Chọn một workflow thay đổi nhiều và áp dụng ba bước:

  1. Làm dữ liệu ranh giới bất biến: xem request/response, message, event là "tạo 1 lần, không sửa".
  2. Refactor lõi thành các biến đổi thuần: dữ liệu vào → dữ liệu ra; đẩy I/O và side effect ra mép.
  3. Giảm cấu trúc mutable chia sẻ: mỗi phần trạng thái có 1 chủ sở hữu, phơi ra qua interface nhỏ.

Đo lường thành công bằng ít lỗi lặt vặt hơn, bán kính ảnh hưởng nhỏ hơn cho mỗi thay đổi, và ít cần phối hợp cẩn thận khi release.

Mục lục
Tại sao độ phức tạp vẫn “thắng” trong các dự án thực tếRich Hickey và điều Clojure muốn khắc phụcĐơn giản: không phải “dễ”, mà là ít phần chuyển động hơnTrạng thái: nhân tố vô hình làm tăng độ phức tạpBất biến giải thích mà không dùng thuật ngữ khoa học máy tínhĐồng thời dễ hơn khi dữ liệu không thay đổi“Mặc định tốt hơn” có nghĩa là gì trong thực tếTách facts khỏi views: thời gian, lịch sử và khả năng truy vếtDữ liệu trước tiên: làm thông tin bền và linh hoạtÁp dụng ý tưởng cho hệ thống phức tạp (không cần rewrite)Nơi nền tảng và công cụ có thể củng cố “mặc định tốt hơn”Các đánh tradeoff, giới hạn, và tránh chủ nghĩaChecklist thực tế để tiến tới phần mềm đơn giản hơnCâu hỏi thường gặp
Chia sẻ
Koder.ai
Build your own app with Koder today!

The best way to understand the power of Koder is to see it for yourself.

Start FreeBook a Demo