5 điểm bởi GN⁺ 2025-06-18 | 1 bình luận | Chia sẻ qua WhatsApp
  • Độ phức tạp là yếu tố nguy hiểm nhất trong phát triển phần mềm
  • Hiệu quả thực sự đến từ cách tiếp cận thực dụng để tránh độ phức tạp, như “giải pháp 80/20”
  • Điều quan trọng là giữ thái độ cân bằng và linh hoạt đối với kiểm thử và tái cấu trúc
  • Nhấn mạnh việc tận dụng công cụ và hình thành thói quen viết mã dễ đọc, dễ bảo trì
  • Cảnh giác với trừu tượng hóa quá mức và các trào lưu, đồng thời khuyến nghị theo đuổi sự đơn giản

Giới thiệu

  • Bài viết này là tập hợp những suy nghĩ của một nhà phát triển có não Grug, đúc kết từ những điều học được qua kinh nghiệm sau thời gian dài làm phần mềm
  • Nhà phát triển có não Grug tự cho rằng mình không thông minh, nhưng đã học được rất nhiều qua nhiều năm lập trình
  • Với mong muốn người khác có thể học từ sai lầm, tác giả chia sẻ những nhận ra của mình theo cách dễ hiểu và hài hước
  • Chính độ phức tạp mới là kẻ thù lớn nhất của đời lập trình
  • Độ phức tạp lén lút xâm nhập vào codebase, khiến cả những đoạn mã ban đầu dễ hiểu cũng dần trở nên không thể chỉnh sửa

Đối phó với con quỷ của độ phức tạp

  • Độ phức tạp len vào một cách im lặng như một linh hồn vô hình, và quản lý dự án hay các nhà phát triển không phải Grug thường khó nhận ra điều đó
  • Cách tốt nhất để ngăn độ phức tạp là nói “không”
    • “Tôi sẽ không làm tính năng này”
    • “Tôi sẽ không đưa lớp trừu tượng này vào”
    • Dĩ nhiên, xét về sự nghiệp thì việc hô “có” có thể có lợi hơn, nhưng nhà phát triển có não Grug coi trọng việc thành thật với chính mình
  • Tùy tình huống vẫn cần thỏa hiệp (“ok”), và trong những trường hợp đó tác giả thích giải quyết vấn đề đơn giản bằng giải pháp 80/20 (áp dụng nguyên lý Pareto)
  • Không cần nói hết mọi thứ với quản lý dự án mà thực tế vẫn làm theo kiểu 80/20 cũng là một chiến lược khôn ngoan

Cấu trúc mã và trừu tượng hóa

  • Đơn vị chia tách phù hợp của mã (cutpoint) sẽ tự nhiên lộ ra theo thời gian, vì vậy tốt hơn là tránh trừu tượng hóa sớm
  • Một cutpoint tốt lý tưởng là có giao diện hẹp với phần còn lại của hệ thống
  • Nỗ lực trừu tượng hóa quá sớm rất dễ thất bại, và các lập trình viên giàu kinh nghiệm thường chỉ từ từ cấu trúc hóa khi hình dạng của mã đã phần nào ổn định
  • Các nhà phát triển ít kinh nghiệm hoặc kiểu “não to” thường cố trừu tượng hóa quá mức ở giai đoạn đầu của dự án, để lại gánh nặng bảo trì

Chiến lược kiểm thử

  • Sự cân bằng trong mức độ ám ảnh với kiểm thử là rất quan trọng
  • Tác giả thích viết test sau khi đã tạo nguyên mẫu và khi mã đã tương đối ổn định
  • Unit test có được dùng ở giai đoạn đầu, nhưng trên thực tế tầng trung gian (kiểm thử tích hợp) mới mang lại hiệu quả lớn nhất
  • Kiểm thử end-to-end cũng cần thiết, nhưng nếu quá nhiều sẽ thành không thể bảo trì, nên chỉ giữ lại một số ít luồng thật sự cần thiết
  • Khi có báo cáo lỗi thì phải thêm test tái hiện lỗi trước rồi mới sửa lỗi

Quy trình, agile, tái cấu trúc

  • Agile không tệ với nhà phát triển Grug, cũng không phải thứ tệ nhất, nhưng đặt kỳ vọng quá mức vào “thầy pháp agile” thì rất nguy hiểm
  • Tạo nguyên mẫu, công cụ và đồng đội tốt mới thực sự là các yếu tố thành công quan trọng hơn
  • Tái cấu trúc cũng là một thói quen tốt, nhưng những đợt tái cấu trúc lớn và gượng ép thì rất rủi ro
  • Việc cố ép đưa vào các lớp trừu tượng phức tạp ngược lại còn dẫn tới thất bại dự án

Bảo trì, chủ nghĩa hoàn hảo và sự khiêm tốn

  • Việc xé ra làm lại hệ thống hiện có mà không có lý do là rất nguy hiểm, và tùy tiện loại bỏ những cấu trúc “không biết vì sao nó tồn tại” không phải là thói quen tốt
  • Chủ nghĩa lý tưởng mơ về mã hoàn hảo trên thực tế phần lớn gây ra vấn đề
  • Càng có kinh nghiệm, người ta càng cảm nhận rõ rằng cần tôn trọng đoạn mã đang chạy được

Công cụ và năng suất

  • Công cụ phát triển tốt (IDE autocomplete, debugger, v.v.) có thể nâng năng suất lên rất nhiều, và điều quan trọng là phải hiểu sâu cách dùng chúng
  • Tác giả nhấn mạnh rằng giá trị thực tế của hệ thống kiểu nằm ở “tự động hoàn thành” và ngăn lỗi, còn trừu tượng hóa và generic quá mức thì ngược lại lại nguy hiểm

Phong cách mã và sự lặp lại

  • Khuyến nghị phong cách như tách điều kiện ra nhiều dòng để mã dễ đọc và dễ debug hơn
  • Tôn trọng nguyên tắc DRY (Don’t Repeat Yourself), nhưng nhấn mạnh rằng cân bằng quan trọng hơn việc cố gắng loại bỏ mã lặp bằng mọi giá
  • Trong nhiều trường hợp, lặp lại đơn giản còn tốt hơn một cách triển khai DRY phức tạp

Nguyên tắc thiết kế phần mềm

  • Thay vì nguyên tắc SoC (tách biệt mối quan tâm), tác giả chuộng tính cục bộ của hành vi, và cho rằng “mã thực hiện hành vi đó nên nằm ở chính đối tượng đó thì sẽ dễ bảo trì hơn”
  • Cảnh báo rằng callback/closure, hệ thống kiểu, generic, trừu tượng hóa v.v. chỉ nên được dùng vừa đủ và đúng chỗ
  • Lạm dụng closure trong JavaScript có thể tạo ra “địa ngục callback”

Logging, vận hành

  • Logging rất quan trọng; nên ghi ở các nhánh chính, và trong môi trường cloud thì nên cấu hình để có thể truy vết bằng request ID v.v.
  • Nếu có thể tận dụng mức log động và log theo từng người dùng thì sẽ rất hữu ích cho việc lần theo sự cố khi vận hành

Đồng thời, tối ưu hóa

  • Với đồng thời, tác giả chỉ tin vào những mô hình đơn giản nhất có thể (web request không trạng thái, hàng đợi worker tách biệt, v.v.)
  • Chỉ nên tối ưu hóa thật sự sau khi đã có dữ liệu profile hiệu năng thực tế
  • Cần cẩn trọng với các chi phí ẩn như network I/O; chỉ nhìn độ phức tạp CPU thôi là rất nguy hiểm

Thiết kế API

  • Một API tốt phải dễ dùng, và thiết kế hay trừu tượng hóa quá phức tạp sẽ làm hỏng trải nghiệm của lập trình viên
  • Tác giả khuyến nghị cấu trúc gồm “API đơn giản phù hợp với use case” và “API phân tầng cho phép xử lý cả các trường hợp phức tạp”

Phát triển parser

  • Recursive descent parser bị đánh giá thấp trong học thuật, nhưng lại là cách phù hợp nhất và dễ hiểu nhất cho mã sản phẩm thực tế
  • Theo kinh nghiệm phát triển parser trong đa số trường hợp, parser sinh bởi công cụ thường cho ra kết quả quá phức tạp, thậm chí phản tác dụng trong việc giải quyết vấn đề
  • Tác giả xem "Crafting Interpreters" là cuốn sách được khuyến nghị hàng đầu, với rất nhiều lời khuyên thực tiễn

Frontend và trào lưu

  • Frontend hiện đại (React, SPA, GraphQL, v.v.) lại thường gọi thêm con quỷ của độ phức tạp và trong nhiều trường hợp là không cần thiết
  • Bản thân Grug thích cách giảm độ phức tạp thông qua các công cụ đơn giản như htmx, hyperscript
  • Ở frontend luôn có các thử nghiệm mới liên tục xuất hiện, nhưng cần lưu ý rằng nhiều thứ chỉ là lặp lại các ý tưởng cũ

Yếu tố tâm lý, hội chứng kẻ mạo danh

  • Phần lớn lập trình viên thường cảm thấy “mình không biết mình đang làm gì”, và cần thoát khỏi hiện tượng FOLD (Fear Of Looking Dumb)
  • Khi một lập trình viên senior công khai nói rằng “cái này ngay cả tôi cũng thấy khó, quá phức tạp”, thì các lập trình viên junior cũng có thể bớt áp lực hơn
  • Hội chứng kẻ mạo danh là cảm xúc rất phổ biến, và tác giả khích lệ rằng ai cũng có thể tiếp tục học hỏi và trưởng thành

Kết luận

  • Trong lập trình, độ phức tạp luôn là thứ phải cảnh giác, và việc giữ sự đơn giản là cốt lõi của phát triển thành công
  • Kinh nghiệm, việc tận dụng công cụ hiệu quả, sự khiêm tốn và thái độ tôn trọng đoạn mã thực sự chạy được sẽ dẫn tới cách phát triển hiệu quả và có giá trị hơn về lâu dài
  • "Độ phức tạp rất, rất tệ" — hãy luôn ghi nhớ câu này

1 bình luận

 
GN⁺ 2025-06-18
Ý kiến trên Hacker News
  • Tôi đánh giá giá trị của một debugger giỏi cao đến mức không gì đong đếm nổi, thực tế còn thấy nó ấn tượng hơn thế. Dù là startup nhỏ hay đội big tech nổi tiếng, rất nhiều lần trong team chỉ có mình tôi dùng debugger. Tôi cũng thấy thực tế là nhiều người vẫn debug bằng câu lệnh print. Mỗi khi muốn chia sẻ workflow của mình với đồng nghiệp thì hầu như không có phản hồi. Tôi đồng ý rằng điểm khởi đầu tốt nhất để hiểu hệ thống chính là debugger. Dừng lại ở một dòng code thú vị khi test và xem stack dễ hơn rất nhiều so với việc lần theo code trong đầu. Nếu học cách dùng debugger, bạn thực sự có được một siêu năng lực nho nhỏ. Nếu có thể, tôi rất khuyến khích thử áp dụng ít nhất một lần
    • Tôi thực sự muốn dùng debugger đúng nghĩa, nhưng với góc nhìn của người chỉ từng làm ở các công ty lớn thì điều đó gần như bất khả thi trong thực tế. Với kiến trúc microservice mesh, bạn không thể chạy mọi thứ ở local, và trong môi trường test thì đa phần cũng được cấu hình để không thể gắn step debugger. Vì vậy print debugging là lựa chọn duy nhất có thể làm. Thậm chí nếu hệ thống log cũng gặp sự cố, hoặc chương trình crash trước khi in log, thì ngay cả print cũng không dùng được
    • Vài năm trước đã có một cuộc thảo luận hay về chủ đề này. Có một câu nói nổi tiếng của Brian Kernighan và Rob Pike, mà cả hai đều không phải lập trình viên trẻ: "Chúng tôi không dùng debugger cho mục đích nào khác ngoài việc xem stack trace hoặc kiểm tra vài giá trị biến. Cấu trúc dữ liệu và luồng điều khiển phức tạp rất dễ khiến người ta mắc kẹt trong tiểu tiết. Suy nghĩ về chương trình kỹ hơn trong đầu, rồi thêm output bằng print và mã tự kiểm tra ở những điểm giữa chừng sẽ hiệu quả hơn. Thêm print nhanh hơn rất nhiều so với bước từng dòng trong debugger. Hơn nữa, mã print còn ở lại trong chương trình, còn phiên debug thì biến mất." Tôi cũng đồng ý với ý kiến này. Trong phần lớn quá trình phát triển, vòng lặp print-giả thuyết-chạy lại giải quyết vấn đề nhanh hơn nhiều. Không phải là tôi "chạy thử" code trong đầu, mà là tôi đã có sẵn mô hình hoạt động của luồng code, nên khi print cho ra kết quả sai thì phần lớn thời gian tôi trực giác được ngay chuyện gì đang xảy ra. Link liên quan: The unreasonable effectiveness of print debugging
    • Trên các hệ Linux, lý do printf debugging luôn phổ biến là vì môi trường GUI debugger không đáng tin. GUI trên Linux thường thiếu ổn định nên khó mà tin tưởng. Với tôi, thời điểm bắt đầu dùng debugger nghiêm túc là khi (1) trên Windows GUI chạy tốt nhưng CLI lại hay lỗi, và (2) sau vài lần mã print debugging vô tình lọt vào bản phát hành và gây sự cố. Sau đó tôi đã có nhiều cuộc phiêu lưu với debugger CLI, và cảm thấy quy trình Junit+debugger (dựa trên IDE như Eclipse), nơi có thể thử ngay đoạn code mang tính thử nghiệm rồi giữ nó lại thành test, tiện không kém gì Python REPL. Tuy vậy, đúng là cần đầu tư ban đầu để thiết lập debugger phù hợp với môi trường
    • Với code của tôi thì dùng debugger rất dễ và tôi thực sự thích nó. Nhưng một khi debugger đi sâu hơn phần code tôi viết, chui vào bên trong library hay framework, thì tôi cũng lập tức lạc lối và ghét nó. Những framework/library như vậy được tạo ra bằng hàng trăm nghìn giờ công, nên với trình độ của tôi thì nó nhanh chóng vượt khỏi phạm vi có thể hiểu được
  • Nếu thầy Carson có đọc được bài này, tôi thật lòng muốn gửi lời cảm ơn. Hồi đại học tôi không hiểu vì sao phải học HTMX, cũng không hiểu vì sao thầy lại nhiệt huyết đến vậy, nhưng vài năm sau tôi mới thực sự ngộ ra. HTML over the wire thực sự là tất cả. Khi làm Staff Ruby on Rails Engineer, tôi đã nhiều lần thấy công trình của thầy trong Hotwire, và thỉnh thoảng thấy thầy hoạt động trên GitHub hay Hacker News thì thật sự rất kinh ngạc. Thầy luôn như một ánh sáng của cộng đồng lập trình. Xin gửi sự kính trọng và biết ơn sâu sắc
    • Không chỉ mình tôi thấy nghẹn lại ở đây, thật sự rất xúc động
    • HTMX chẳng phải chỉ là một meme thôi sao? Vì Poe’s Law nên tôi không rõ đây là nghiêm túc hay không
  • Bài này có rất nhiều câu để đời, nhưng tôi thích nhất đoạn nói về microservice: "grug không hiểu vì sao big brain đã khó tách hệ thống cho ra hồn, mà còn cố thêm cả network call vào"
    • Có những người chỉ biết một cách để chia hệ thống thành các phần, đó là biến chúng thành API. Nếu không được lộ ra dưới dạng API thì với họ nó chỉ là đống code đen hộp, không thể hiểu và cũng không tái sử dụng được
    • Cũng hơi tiếc là microservice đôi khi lại được dùng vì nó thực sự thực dụng trong một số hoàn cảnh
    • Tôi liên tục thấy một team dev nhỏ chỉ có hai người, với một web app tí hon chỉ có năm cái form, vẫn cố làm mọi thứ phức tạp thành kiến trúc “microservice” kiểu chia sẻ database, quản lý API, batch job qua queue, thông báo email, thêm cả nền tảng Observability tự làm, v.v. Và cuối cùng thì ngay cả form bình thường cũng bị biến thành SPA vì “như thế dễ hơn”. Giờ tôi đã hiểu rằng “architecture” và “pattern” chỉ là cách để những lập trình viên vô dụng tự tạo việc cho mình. Nếu không có mấy thứ đó thì họ đã đứng ngoài đường cầm biển “cho tôi một miếng sandwich cũng được, miễn là tôi được viết JavaScript”
    • Một thuyết âm mưu của tôi là pattern microservice ra nông nỗi này vì được các cloud vendor đẩy mạnh. - Không có orchestrator như K8S thì gần như không chạy nổi, nên rất dễ bán cloud managed service - Nhiều network traffic/CPU hơn nên tiền tính phí cũng cao hơn - Chia sẻ trạng thái quy mô lớn trở nên khó, kéo theo nhu cầu database/event queue managed - Khó chạy local, nên cả môi trường dev cũng bị kéo sang chi phí cloud - Bị trói vào cách làm riêng của cloud nên khó thoát ra. Ngày xưa cloud từng được quảng cáo là giúp tiết kiệm chi phí IT, nghe thật nực cười. Từ những năm 2000 tôi đã biết đó là ảo tưởng, và kết cục chỉ là mọi thứ đều đắt hơn
  • Câu "phức tạp vs Tyrannosaurus đấu mặt đối mặt, grug chọn Tyrannosaurus thay vì phức tạp: ít ra Tyrannosaurus còn nhìn thấy được" gây ấn tượng mạnh đến mức tôi nhớ lại nó ít nhất mỗi tuần một lần
    • Trích đoạn: "Ngay cả khi ngã xuống, Leyster vẫn không buông cái xẻng. Trong cơn hoảng loạn, anh quên mất chuyện đó. Vì thế anh tuyệt vọng vung cái xẻng vào chân con Tyrannosaurus non...". Đây là một cảnh miêu tả cực kỳ sống động cuộc chiến sinh tồn khốc liệt với Tyrannosaurus. Cuối cùng đồng đội Tamara đã dũng cảm đâm thẳng ngọn giáo vào chính giữa mặt con Tyrannosaurus để vượt qua nguy hiểm. Cảnh chiến đấu, căng thẳng, rồi sự im lặng sau đó đều rất ấn tượng
    • Rõ ràng grug chưa từng phải chiến đấu với một con Tyrannosaurus “vô hình”. Còn tôi thì hiện giờ vẫn đang solo 1v1 với Tyrannosaurus vô hình, cực kỳ khổ sở
  • Điều đáng khâm phục ở bài này là tác giả có thể làm những thứ “phức tạp hơn”, nhưng bằng trải nghiệm thực tế lại chọn không đi theo con đường đó. Tất nhiên vẫn có lúc và có chỗ cần đến trừu tượng hóa hay độ phức tạp, nhưng triết lý grug nói rằng bản thân những thứ đó không có giá trị nội tại. Tôi thấy điểm này thật sự rất có lý. AI dường như cũng hoạt động hiệu quả hơn với code nhất quán và dựa trên dữ liệu
    • Khi dùng độ phức tạp và trừu tượng hóa, đó nên là lúc chúng khiến code dễ hiểu hơn trước. Hãy luôn nhớ tiền đề là “không cần thêm một khóa học đặc biệt chỉ để hiểu nó”. (Tùy hoàn cảnh)
    • "Mọi thứ nên được làm đơn giản nhất có thể, nhưng không được đơn giản quá mức"
  • Khó tin đây là bài viết từ năm 2022. Cảm giác như tôi đã đọc nó từ 10 năm trước và luôn nghĩ đó là một “kinh điển”
  • Bài luận này là bài viết tôi thích nhất về việc làm phần mềm. Văn phong cũng rất cuốn hút (dù có người có thể thấy khó chịu), và cốt lõi của nó lúc nào cũng đúng
  • Đoạn code kiểu "đáng buồn nhưng là sự thật: học cách nói 'yes', học cách đổ lỗi cho grug khác khi thất bại, chiến lược sự nghiệp đỉnh cao" quá đúng với thực tế. Ban đầu tôi còn tưởng nguyên nhân chỉ là vấn đề giao tiếp trong đội kỹ thuật ở công ty, nhưng theo thời gian tôi học ra rằng (giống grug) mọi chuyện đúng là như vậy
  • Trong tất cả những phần giải thích về visitor pattern mà tôi từng đọc, nội dung trong bài này là hay nhất
    • Tôi không làm trong codebase OO điển hình nên không thực sự biết visitor pattern là gì, nhưng tôi muốn giới thiệu cuốn "Crafting Interpreters", một cuốn sách về việc xây dựng interpreter/VM. Trong sách có chỉ ra visitor pattern thực sự được dùng như thế nào. Tôi đã tự đọc để cố hiểu vì sao lại cần mức độ phức tạp đó, nhưng cuối cùng vẫn thay bằng tagged union. Có lẽ là vì tôi yếu về OO, nhưng trọng tâm của bài grug cũng là như thế này. Khi không cần tự chuốc lấy sự phức tạp và tính gián tiếp, vẫn có những cách trực quan hơn
    • Tôi khá nhạy cảm với chuyện đặt tên, và tôi không thích cái tên visitor pattern vì nó quá mơ hồ. Trên thực tế tôi chưa bao giờ đặt tên thứ gì là Visitor cả. Ví dụ nếu là bài tập về cây cú pháp (AST), thì thay vì Visitor, những cái tên như AstWalker, AstItem::dispatch(AstWalker), AstWalker::process(AstItem) có ý nghĩa cụ thể hơn nhiều. “visitor” theo nghĩa “đi thăm” quá trừu tượng và vô nghĩa. Tùy ngữ cảnh mà nên khác nhau, và chỉ cần ghi chú là “visitor pattern” thì cũng đủ để người khác nhận ra. Trước đây khi phải đối chiếu hai cây đối tượng để so sánh/import dữ liệu, tôi từng dùng tên AbstractImporter. Nó cụ thể hơn, quy trình và vai trò cũng rõ ràng hơn. Dù vậy, nó cũng không giống visitor pattern điển hình
    • Tôi tra thử thì thấy có người đánh giá là “Bad”. Haha
  • Chia sẻ thêm bài liên quan. Có ai có ý kiến khác hoặc bài viết bổ sung không?<br/><i>The Grug Brained Developer (2022)</i> - https://news.ycombinator.com/item?id=38076886 - Tháng 10 năm 2023 (192 bình luận)<br/><i>The Grug Brained Developer</i> - https://news.ycombinator.com/item?id=31840331 - Tháng 6 năm 2022 (374 bình luận)