5 điểm bởi GN⁺ 2025-06-18 | 1 bình luận | Chia sẻ qua WhatsApp
  • Thông qua trải nghiệm của một lập trình viên “não nhỏ” đã lập trình lâu năm, bài viết nhấn mạnh rằng kẻ thù lớn nhất của phát triển phần mềm là độ phức tạp, và thái độ giảm bớt nó phải là trọng tâm của thực tế công việc
  • Vũ khí cơ bản để ngăn độ phức tạp là nói “no”, và khi cần thỏa hiệp thì tìm lời giải thực tế bằng giải pháp 80/20, prototype, refactoring nhỏ và trừu tượng hóa muộn
  • Với kiểm thử, sau khi code đã tương đối ổn định, nên đặt kiểm thử tích hợp làm trung tâm; dùng kiểm thử đơn vị và một số ít kiểm thử end-to-end để hỗ trợ; còn bug thì trước hết tái hiện bằng kiểm thử hồi quy rồi mới sửa
  • Công cụ, type system, logging, debugger và API đơn giản giúp giảm gánh nặng ghi nhớ và suy luận của lập trình viên, nhưng generic, callback, microservice và framework frontend quá mức có thể làm tăng độ phức tạp
  • Cần tiếp cận code hiện có và quy trình tổ chức với sự khiêm tốn, không tùy tiện xóa code mà mình chưa hiểu, và cần một văn hóa có thể nói “quá phức tạp”

Độ phức tạp là kẻ thù vĩnh viễn của lập trình viên

  • Kẻ thù nguy hiểm nhất trong phát triển phần mềm là độ phức tạp
    • Độ phức tạp len lỏi dần vào codebase, khiến khi thay đổi một chỗ thì cả những nơi tưởng như không liên quan cũng bị hỏng
    • Grug ví nó như một “con quỷ” vô hình, một thứ mà lập trình viên không thể trực tiếp nhìn thấy hay dễ dàng đánh bại
  • Độ phức tạp cũng có thể đi vào thông qua những lập trình viên hay project manager có thiện ý
    • Càng nhiều tính năng, trừu tượng hóa và quy trình, code càng khó hiểu
    • Bản thân Grug cũng thừa nhận đôi khi chính mình là người đưa độ phức tạp vào

Cách dùng “no” và “ok”

  • Vũ khí mạnh nhất để ngăn độ phức tạp là “no
    • Không xây tính năng không cần thiết
    • Không tạo trừu tượng hóa không cần thiết
  • Tuy nhiên, dù “no” là lời khuyên kỹ thuật tốt, không phải lúc nào nó cũng có lợi cho sự nghiệp
    • “yes” có thể dẫn đến nhiều phần thưởng hơn hoặc vị trí quản lý
    • Dù vậy, để trung thực với chính mình với tư cách lập trình viên, “no” vẫn rất quan trọng
  • Khi cần thỏa hiệp, hãy trả lời “ok” rồi tìm giải pháp 80/20
    • 80/20 solution là cách tiếp cận dùng 20% lượng code để đáp ứng 80% yêu cầu
    • Dù không có đủ mọi tính năng trang trí, nó vẫn có thể truyền tải phần lớn giá trị và kìm hãm độ phức tạp
    • Đôi khi tốt hơn là giải quyết bằng cách đơn giản mà không giải thích mọi chi tiết triển khai cho project manager

Cấu trúc hóa code và refactoring

  • Không nên chia nhỏ ứng dụng quá sớm ở giai đoạn đầu dự án
    • Ban đầu, hình dạng của hệ thống vẫn chưa rõ ràng, và cả việc đang xây cái gì cũng chưa được hiểu hoàn toàn
    • Theo thời gian, các điểm cắt (cut point) tốt sẽ lộ ra
  • Điểm cắt tốt có interface hẹp với phần còn lại của hệ thống
    • Nó che giấu độ phức tạp bên trong bằng số lượng nhỏ hàm hoặc trừu tượng hóa
    • Grug ví điều này với việc nhốt con quỷ phức tạp trong viên pha lê
  • Khi lập trình viên não lớn cố tạo nhiều trừu tượng hóa ngay từ đầu dự án, cần có cách giảm thiểu thiệt hại
    • Có thể hướng họ sang các sản phẩm đầu ra không gây hại trực tiếp cho code, như sơ đồ UML
    • Nếu yêu cầu “demo chạy được vào ngày mai”, họ sẽ nhanh chóng nhìn thấy code thực sự hoạt động và thực tế
    • Cách tiếp cận demo này có thể gọi là prototype
  • Refactoring hữu ích ở giai đoạn sau của dự án, khi code đã ổn định
    • Refactoring càng lớn thì khả năng thất bại càng cao
    • Nên tiến hành từng bước nhỏ trong khi giữ cho hệ thống tiếp tục hoạt động
    • Kiểm thử end-to-end trở thành dây cứu sinh trong quá trình refactoring, nhưng khi nó hỏng thì có thể khó hiểu nguyên nhân
  • Trừu tượng hóa quá mức có thể dẫn đến thất bại trong refactoring và thất bại của hệ thống
    • J2EE được nêu như một ví dụ về trừu tượng hóa quá mức
    • Việc đưa OSGi vào là một nỗ lực nhằm giảm độ phức tạp, nhưng ngược lại đã tạo ra độ phức tạp còn mạnh hơn, và được cho là đã cần nhiều người-năm để làm lại

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

  • Kiểm thử tiết kiệm rất nhiều thời gian, nhưng tác giả hoài nghi với “luôn viết test trước”
    • Khi chưa hiểu domain, rất khó biết nên kiểm thử cái gì
    • Grug thích viết phần lớn test sau prototype, khi code bắt đầu ổn định
  • Dù viết test muộn, vẫn cần kỷ luật
    • Không được bỏ qua test chỉ vì “máy tôi chạy được”
    • Không có gì đảm bảo nó sẽ chạy trên máy khác, hoặc trên chính chiếc máy đó trong tương lai
  • Phân biệt vai trò của kiểm thử đơn vị, kiểm thử tích hợp và kiểm thử end-to-end
    • unit tests hữu ích ở giai đoạn đầu dự án, nhưng dễ vỡ khi triển khai thay đổi và có thể làm refactoring khó hơn
    • Kiểm thử end to end cho thấy hành vi của toàn hệ thống, nhưng khi hỏng thì khó xác định nguyên nhân, và nếu thường xuyên hỏng thì có thể bị phớt lờ
    • integration tests đủ cao để xác nhận tính đúng đắn của hệ thống, và đủ thấp để xem nguyên nhân bằng debugger, nên gần với “điểm ngọt”
  • Hình thức lý tưởng của bộ test như sau
    • Ban đầu có một số kiểm thử đơn vị
    • Khi điểm cắt xuất hiện và hệ thống ổn định hơn, có kiểm thử tích hợp mạnh
    • Một bộ kiểm thử end-to-end nhỏ, được quản lý tốt, chỉ bao gồm các chức năng UI phổ biến nhất và những edge case quan trọng
  • Ưu tiên chỉ dùng mocking hiếm khi, và ngay cả khi cần thì cũng chỉ dùng ở các đơn vị lớn như ranh giới hệ thống
  • Khi sửa bug, ngoại lệ là trước tiên viết kiểm thử hồi quy để tái hiện bug rồi mới sửa

Quy trình, Agile và thái độ với code hiện có

  • Agile không phải tệ nhất, nhưng cũng khó nói là chỉ toàn tốt
    • Nó có thể khá ổn ở một mức nào đó như cách tổ chức phát triển
    • Cần cảnh giác với các chuyên gia Agile, những người mỗi lần thất bại lại nói “đã không làm Agile đúng cách”
  • Prototype, công cụ và tuyển lập trình viên giỏi quan trọng hơn cho thành công
    • Quy trình Agile có thể hữu ích, nhưng nếu xem nó quá nghiêm túc thì có thể gây hại
    • no silver club, tức là không có viên đạn bạc nào giải quyết mọi vấn đề phần mềm
  • Chesterton’s Fence được dùng như một cảnh báo về việc xóa code hiện có
    • Nếu không biết công dụng của một hàng rào, trước hết phải hiểu nó, không được dỡ bỏ ngay
    • Ngay cả code xấu xí, nếu hôm nay nó đang chạy, cũng cần được tôn trọng
    • Đặc biệt với hệ thống lớn, phải hiểu trước rồi mới cải thiện
  • Test có thể là gợi ý cho biết vì sao một “hàng rào” nào đó tồn tại

Công cụ và type system

  • Công cụ giúp giảm gánh nặng mà lập trình viên phải tự ghi nhớ và suy luận
    • Khi vào môi trường mới, dành thời gian học các công cụ xung quanh có thể giúp tăng năng suất
    • Nếu không có tài liệu, có thể cần hỏi các lập trình viên khác để nắm được
  • Code completion của IDE giúp không cần nhớ toàn bộ API
    • Tác giả nói rằng lập trình Java gần như không thể nếu không có code completion
  • Debugger tốt là cực kỳ quan trọng
    • Cần học sâu các tính năng như conditional breakpoint, đánh giá biểu thức và duyệt stack
    • Grug cho rằng với lập trình viên mới, việc học debugger có thể dạy họ về máy tính nhiều hơn cả các lớp đại học
  • Giá trị lớn nhất của type system là “khi nhấn dấu chấm (.) thì hiện ra những gì có thể làm”
    • Tính đúng đắn của kiểu cũng tốt, nhưng với Grug, hỗ trợ công cụ và code completion có giá trị lớn hơn
    • Trừu tượng hóa kiểu hoặc generic quá mức có thể khiến code công việc thực tế trở nên khó khăn
    • Grug thích giới hạn việc dùng generic chủ yếu trong các container class

Biểu thức, DRY, separation of concerns

  • Ưu tiên code dễ debug hơn code ngắn
    • Thay vì viết điều kiện phức tạp trên một dòng, nếu tách thành các biến trung gian thì dễ xem kết quả và ý nghĩa của từng biểu thức hơn
    • Dù số dòng tăng lên, việc hiểu và debug câu điều kiện sẽ dễ hơn
  • DRY là lời khuyên tốt, nhưng cần cân bằng
    • Code lặp lại đơn giản và rõ ràng có thể tốt hơn callback, closure hay object model phức tạp
    • Nếu cố loại bỏ lặp lại mà làm tăng độ phức tạp thì đó là thiệt hại
  • Với Separation of Concern, tác giả phê phán hơn
    • Ví dụ điển hình trong phát triển web là tách CSS, HTML và JavaScript
    • Grug thích phương án locality of behavior hơn
    • Nếu đặt code liên quan gần đối tượng mà nó tác động, khi nhìn đối tượng đó ta sẽ biết ngay nó làm gì
  • Closure hữu ích trong các mục đích đúng, như trừu tượng hóa thao tác trên collection
    • Nhưng giống như muối, type system và generic, chỉ cần dùng một chút là đủ; quá nhiều thì có hại
    • “callback hell” trong JavaScript được nêu như ví dụ về lạm dụng closure

Logging, concurrency và tối ưu hóa

  • Logging đặc biệt quan trọng trong môi trường triển khai cloud
    • Ghi log ở mỗi nhánh logic chính
    • Nếu một request đi qua nhiều máy, hãy đưa request ID vào mọi log để có thể liên kết chúng
    • Nếu có thể, điều chỉnh log level một cách động
    • Nếu có thể, cũng điều chỉnh log level theo từng người dùng
  • Thư viện logging Java có thể phức tạp, nhưng nếu đầu tư đúng mức vào hạ tầng logging thì về sau sẽ được đền đáp lớn
  • Concurrency là thứ cần phải sợ
    • Dùng các mô hình đơn giản nhất có thể, như web request handler không trạng thái và hàng đợi tác vụ từ xa độc lập
    • optimistic concurrency được xem là hoạt động tốt trong lĩnh vực web
    • thread local variable chủ yếu đôi khi được dùng khi viết code framework
    • Ngay cả cấu trúc dữ liệu concurrency như ConcurrentHashMap của Java vẫn phải được dùng cẩn thận
  • Tối ưu hóa chỉ nên bắt đầu khi có profile hiệu năng thực tế
    • Đồng ý với câu “premature optimization is the root of all evil”
    • Nút thắt thực tế có thể khác với dự đoán
    • Không nên chỉ nhìn CPU; các lời gọi mạng có thể tương đương hàng triệu chu kỳ CPU, nên cần giảm chúng khi có thể

API, parsing và visitor pattern

  • API tốt khiến lập trình viên không phải suy nghĩ nhiều
    • API xấu xuất hiện khi nó được thiết kế xoay quanh nội bộ triển khai hoặc domain model, hoặc khi nó quá trừu tượng
    • Grug thích phân tầng: cung cấp API đơn giản cho trường hợp đơn giản, và API phức tạp hơn cho trường hợp phức tạp
  • Với API hướng đối tượng, tốt hơn là gắn chức năng vào đối tượng đích
    • Tác giả nêu dòng chảy phải đổi list sang Stream rồi collect lại thành List để lọc list trong Java như một ví dụ xấu
    • Tác vụ phổ biến như filter() nên nằm trên list và trả về list
  • Parser recursive descent là một cách thú vị và đẹp
    • Tác giả phê phán parser generator là khó hiểu và khó debug
    • Ông khẳng định parser production thực tế gần như luôn là recursive descent
  • Khuyến nghị đọc Crafting Interpreters của Bob Nystrom
    • Có thể xem miễn phí online, nhưng cũng khuyến nghị mua sách
    • Tuy nhiên, visitor pattern được mô tả là một cái bẫy
  • Đánh giá về Visitor pattern ngắn gọn là “bad”

Frontend, microservice và trào lưu

  • Microservice nguy hiểm ở chỗ “nó thêm cả lời gọi mạng vào bài toán khó nhất là chia hệ thống cho đúng”
  • Trong phát triển frontend, độ phức tạp đặc biệt mạnh
    • Tác giả phê phán tình trạng ngay cả form lưu đơn giản hay website brochure cũng dùng thư viện SPA, GraphQL JSON API và backend HTTP
    • Ông mô tả rằng khi tách frontend và backend, độ phức tạp sẽ có hai nơi cư trú
  • Grug nói rằng mình đã tạo htmxhyperscript để giảm độ phức tạp
    • Ông thích hướng duy trì HTML đơn giản và không dùng nhiều JavaScript
    • Ông thừa nhận React có thể tốt hơn cho việc kiếm việc làm và một số loại ứng dụng nhất định
  • Có nhiều trào lưu trong phát triển, đặc biệt là ở frontend
    • Backend đã trở nên nhàm chán hơn, và nhiều ý tưởng tệ đã được thử rồi
    • Hãy tiếp cận các phương pháp mới mang tính cách mạng một cách thận trọng, như chỉ rắc một hạt muối
    • Nhiều ý tưởng đã từng được thử ít nhất một lần, và các ý tưởng tệ được tái chế có thể làm lãng phí thời gian

Nỗi sợ và hội chứng kẻ mạo danh

  • Việc một senior developer công khai nói “cái này quá phức tạp” là điều tốt
    • Fear Of Looking Dumb(FOLD), lập trình viên khó nói rằng họ không hiểu
    • Khi senior thừa nhận trước, junior cũng có thể nói về sự phức tạp và việc mình chưa hiểu
  • FOLD là nguồn chính giúp độ phức tạp có thêm sức mạnh, đặc biệt với lập trình viên trẻ
    • Óc hài hước và thái độ ghi nhớ những thất bại trong quá khứ được xem là hữu ích
  • Hội chứng kẻ mạo danh cũng phổ biến trong phát triển
    • Grug mô tả mình ở giữa cảm giác đang thống trị mọi thứ và cảm giác không biết mình đang làm gì
    • Dù có thành quả mã nguồn mở của htmx và _hyperscript, ông vẫn sợ sai lầm và thất bại
  • Nếu mọi người đều cảm thấy hội chứng kẻ mạo danh, tốt hơn nên chấp nhận theo kiểu không ai thật sự là kẻ giả mạo

Đọc thêm được khuyến nghị và kết luận

1 bình luận

 
GN⁺ 2025-06-18
Ý kiến trên Hacker News
  • Nếu Giáo sư Carson đang đọc các bình luận, tôi thật lòng muốn cảm ơn tất cả những đóng góp của thầy trong suốt thời gian qua
    Hồi đại học, tôi không hiểu vì sao mình lại học HTMX, cũng không hiểu vì sao thầy lại hào hứng đến vậy, nhưng vài năm trôi qua thì giờ tôi đã hiểu. Tất cả là HTML over the wire
    Khi làm Staff Ruby on Rails Engineer, tôi đã thấy công trình của thầy trong Hotwire, và cũng thật tuyệt khi thỉnh thoảng thấy thầy xuất hiện trên Hacker News hoặc trò chuyện với các lập trình viên Hotwire trên GitHub. Thầy được vô cùng kính trọng và biết ơn như một ánh sáng của cộng đồng lập trình

  • Tôi đồng cảm với câu “một debugger tốt đáng giá như một viên đá lấp lánh, thật ra còn hơn thế nữa”
    Tôi đã trải qua cả startup nhỏ lẫn các đội big tech “tinh hoa”, nhưng gần như lúc nào trong đội cũng chỉ có mỗi tôi dùng debugger. Trong thế giới thực, ít nhất là ở mảng công nghệ web, có vẻ đa số đều debug bằng câu lệnh print
    Việc dừng ở một dòng code thú vị trong lúc chạy test và xem call stack đã đi đến đó như thế nào dễ hơn rất nhiều so với việc chạy xuôi code trong đầu. Với grug trẻ, kỹ năng này là một siêu năng lực nhỏ, nên nếu có thể thì rất đáng dành thời gian để làm cho nó hoạt động được trong codebase của mình

    • Vài năm trước đã có một cuộc thảo luận hay về chủ đề này, và bình luận đứng đầu đã chia sẻ trích dẫn của Brian Kernighan và Rob Pike. Cả hai đều khó mà gọi là grug trẻ
      Họ nói rằng họ không dùng debugger nhiều ngoài mức xem stack trace hoặc giá trị của một hai biến, vì dễ sa vào chi tiết của các cấu trúc dữ liệu và luồng điều khiển phức tạp; họ cho rằng chèn các câu lệnh xuất ra và code tự kiểm chứng ở những điểm then chốt thì hiệu quả hơn.
      Tôi nhìn chung cũng đồng ý. Trong hầu như mọi việc tôi làm, vòng lặp giả thuyết-log-chạy giúp đi đến câu trả lời nhanh hơn nhiều. Không phải tôi đang cố chạy code trong đầu, mà là tôi đã có một mô hình làm việc về cách code đang chạy; nếu mô hình đó đúng thì tôi biết đầu ra nên là gì, và từ đầu ra sai tôi có thể nhanh chóng linh cảm được tình hình thực tế
      [0] The unreasonable effectiveness of print debugging (349 points, 354 comments) April 2021 https://news.ycombinator.com/item?id=26925570
    • Tôi rất muốn dùng debugger thật sự, nhưng từ góc nhìn của người chỉ làm ở các tập đoàn lớn, đó không phải là một lựa chọn
      Với kiến trúc microservices mesh, ngay cả việc chạy một thứ gì đó đúng cách ở local cũng khó, và môi trường test thường cũng không được cấu hình để có thể gắn debugger chạy từng bước vào. Cuối cùng chỉ còn debug bằng print, và nếu chính hệ thống logging có vấn đề hoặc chương trình chết trước khi flush log thì thậm chí cách đó cũng không dùng được
    • Một trong những việc đầu tiên tôi làm khi vào một codebase là thiết lập môi trường IDE/trình soạn thảo để có thể nhanh chóng chạy chương trình bằng debugger, dù lúc đó chưa phải đang sửa vấn đề trước mắt
      Chẳng mấy chốc sẽ cần đến nó. Nhưng ngay cả khi cộng tác, tôi vẫn bối rối khi thấy có những người không hề biết cách dùng debugger. Dù nói “đặt breakpoint ở đó”, “giờ step vào trong hàm rồi xem trạng thái biến”, “cái đó thì step over đi” thì lần nào cũng nhận lại vẻ mặt ngơ ngác
    • Tôi cũng thuộc nhóm gần như không dùng debugger
      Nói chính xác thì tôi không chèn rồi xóa các câu lệnh print, mà chèn code logging sẽ được để lại lâu dài. Nếu là interface quan trọng, tôi thường bắt đầu bằng việc ghi log ở mức INFO cho lúc vào/ra hàm và giá trị tham số, rồi khi sử dụng hệ thống, nếu thấy chỗ nào cần xem kỹ hơn thì thêm log chi tiết hơn
      Tôi cũng khá chăm chút cho định dạng log. Khi làm việc với hệ thống phân tán, việc căn chính xác tiền tố của từng dòng log rất hữu ích. Tôi đưa node ID, pid và timestamp vào dạng độ rộng cố định, rồi tải log của toàn cụm về và sắp xếp, khi đó hành vi của nhiều node hiện ra như được interleave trong một file
    • Dùng debugger với code của chính tôi thì dễ và tôi thích việc đó
      Nhưng ngay khi debugger đi sâu vào thư viện hoặc framework mà tôi dùng, tôi lập tức lạc lối và thấy khó chịu. Framework hay thư viện đó là thứ tích lũy hàng chục nghìn giờ công, nên tôi có cảm giác nó vượt xa trình độ của mình
  • Ở đây có rất nhiều câu như ngọc, nhưng câu này về microservices là mình thích nhất: “grug thắc mắc vì sao những người thông minh lại lấy bài toán khó nhất là chia hệ thống cho đúng, rồi còn nhét thêm cả lời gọi mạng vào đó”

    • Mình cứ phải giải thích điều này cho các nhóm phát triển nhỏ chỉ 1~2 người
      Họ sẵn sàng tách một web app nhỏ nhặt có chừng năm form thành “microservices”, bắt chúng dùng chung cùng một cơ sở dữ liệu, gắn thêm tầng quản lý API, queue cho các tác vụ batch khối lượng lớn (megabyte), hệ thống thông báo email, cả nền tảng observability tự làm, rồi biến các web form đơn giản thành SPA với lý do “như thế dễ hơn”
      Giờ thì mình hiểu “kiến trúc” và “pattern” là chương trình tạo việc làm cho những lập trình viên vô dụng. Nếu không có nó, chắc họ đã đứng ngoài đường cầm biển “cho sandwich thì viết JavaScript”
    • Có vẻ một số người không biết cách nào khác để chia hệ thống thành các phần nhỏ hơn
      Với họ, nếu không được phơi ra dưới dạng lời gọi API thì nó chỉ là một cục code mờ đục không thể hiểu hay tái sử dụng
    • Ranh giới mạng cung cấp một công cụ phân rã mà hầu hết hệ thống module của các ngôn ngữ không có
      Nhiều package có thể hợp tác nội bộ nhưng chỉ phơi ra một API nhỏ cho phần còn lại của codebase. Việc đó là mạng buộc các module chỉ trao đổi dữ liệu, không phải callback hay hành vi, và tạo áp lực để interface tiến hóa theo cách tương thích ngược. Nhờ vậy có thể “hot reload” các module khác nhau vào những thời điểm khác nhau mà không nổ tung
      Có vẻ có thể đạt được phần lớn điều này mà không cần hop mạng thật, nhưng mình vẫn chưa thấy nỗ lực nghiêm túc nào
    • Mình có một thuyết âm mưu rằng microservices là pattern được các công ty cloud thúc đẩy để khiến mọi người xây các ứng dụng như thế này
      Không thể chạy nếu không có orchestrator như K8S, lại khó cài đặt và vận hành, nên rất có lợi cho việc bán cloud được quản lý. Nó khiến người ta dùng nhiều băng thông mạng và CPU hơn, mà cả hai đều là thứ bị tính phí
      Nó khiến việc chia sẻ và duy trì trạng thái phức tạp hoặc lớn bên trong ứng dụng trở nên khó khăn, để rồi người ta dùng các dịch vụ cơ sở dữ liệu được quản lý hoặc event queue thay thế. Nếu là monolith thì chỉ cần dùng queue hoặc channel, nhưng với microservices bạn sẽ muốn một con quái vật khổng lồ như Kafka
      Chạy local cũng khó hơn, nên cần môi trường phát triển trên cloud, và có thể phải có nhiều môi trường dev/test. Nó làm tăng sự phụ thuộc vào cloud vì ứng dụng dựa vào các đặc tính như cách networking của một cloud cụ thể
      Không biết còn ai nhớ thời cloud được bán như thứ giúp giảm chi phí IT không. Đúng là buồn cười; từ những năm 2000 mình đã biết đó là nhảm nhí và nghĩ cuối cùng nó sẽ làm mọi chi phí phình to hơn
    • Mình cho rằng chủ yếu đây là cách chia hệ thống giữa các team
      Vì cách đó dễ quản lý hơn, và nó gần với cách tổ chức đội ngũ phát triển hơn là một quyết định kỹ thuật. Nếu phương án thay thế là monorepo, cá nhân mình thấy monorepo còn tệ hơn
  • “Nếu phải chọn giữa độ phức tạp và đấu 1:1 với khủng long bạo chúa, grug chọn khủng long bạo chúa. Ít nhất grug còn nhìn thấy khủng long bạo chúa”
    Mình nghĩ đến câu này ít nhất mỗi tuần một lần

    • Chắc chắn grug chưa từng đối đầu với khủng long bạo chúa vô hình
      grug này đang đấu 1:1 với khủng long bạo chúa trong suốt, và đã bị nguyền rủa
    • Đọc câu đó xong có cảm giác con mắt thứ ba của mình được mở ra. Thật sự rất truyền cảm hứng
  • Một giá trị của bài này là ở chỗ, người có khả năng làm những thứ tinh vi và phức tạp hơn lại cố ý tránh chúng dựa trên kinh nghiệm
    Rõ ràng có lúc và có nơi cần sự tinh vi cùng các tầng trừu tượng cao hơn. Nhưng triết lý grug nói rằng bản thân việc làm những thứ đó không có giá trị nội tại, và đây có vẻ là lời khuyên khá hợp lý
    Mình cũng thấy các trợ lý AI hiệu quả hơn với code nhất quán, bình thường và lấy dữ liệu làm trung tâm. Tùy hoàn cảnh có thể khác

    • Điều này rất khớp với meme đường cong chuông
      Lập trình viên mới viết code đơn giản, lập trình viên trung cấp viết code phức tạp, còn lập trình viên chuyên gia lại quay về viết code đơn giản
    • Thời điểm và nơi nên dùng sự tinh vi và trừu tượng là khi chúng thực sự làm code dễ hiểu hơn, chứ không phải theo kiểu phải nghe một bài giảng đặc biệt trước mới hiểu được
      Việc có thể mặc định rằng người ta đã nghe bài giảng nào đó hay chưa còn tùy từng bối cảnh
    • “Mọi thứ nên được làm đơn giản hết mức có thể, nhưng không được đơn giản hơn mức đó”
  • Một trong những nghịch lý của phát triển phần mềm hiện đại là chúng ta đưa độ phức tạp vào vì nghĩ rằng “rốt cuộc nó sẽ tiết kiệm thời gian”
    Đôi khi điều đó đúng và thực sự tiết kiệm thời gian, nhưng không phải lúc nào cũng vậy, và có thể thường là không
    DRY đôi khi dẫn tới trừu tượng hóa quá sớm. Khoảnh khắc bạn nghĩ “mẫu này chắc cũng sẽ được dùng ở chỗ khác, nên mình nên tách phần chung ra”, con quỷ phức tạp liền chui vào
    Chúng ta muốn bắt càng nhiều bug càng tốt ở thời điểm biên dịch, nhưng để làm vậy compiler phải biết nhiều hơn về điều ta thực sự định làm, và ta sẽ tạo ra những kiểu dữ liệu phức tạp bào mòn khả năng hiểu
    Ta tạo macro hoặc DSL phức tạp để tránh boilerplate, nhưng vì quy luật trừu tượng bị rò rỉ, đến lúc phải hiểu implementation thật sự thì đầu óc nổ tung
    Điều khó là tất cả các ví dụ này đôi khi lại là ý tưởng hay. Tôi cho rằng dấu hiệu của một kỹ sư phần mềm giỏi là biết phán đoán khi nào đưa độ phức tạp vào thì lại trở thành đơn giản hóa

    • Theo kinh nghiệm, SPoT (single point of truth, một nguồn sự thật duy nhất) là châm ngôn tốt hơn DRY
      Business logic lý tưởng nên được định nghĩa ở một nơi, nhưng những thứ khác thì có thể lặp lại nếu cần, và bản thân việc đó không xấu
      Để tiết chế DRY, tôi cũng nhấn mạnh “quy tắc số 3”. Code copy/paste đến lần thứ ba vẫn ổn, sau đó mới đáng cân nhắc trừu tượng hóa. Tất nhiên không có quy tắc kinh nghiệm nào phù hợp mọi trường hợp, và cảm giác đó rất khó dạy
    • Trung bình thì tôi tin phần lớn code vẫn chưa đủ DRY, nhưng trong các dự án cá nhân gần đây tôi đặt ra nguyên tắc “không có ứng dụng, chỉ có màn hình”. Điều thú vị là tôi đang dùng HTMX, có vẻ do chính tác giả blog này tạo ra
      Thường tôi xây web app bằng framework kiểu Sinatra như Flask, và viết các hàm phản hồi theo pattern URL. Một “màn hình” có thể gồm một hoặc nhiều hàm hoạt động cùng nhau và các template HTML liên quan
      Nếu ứng dụng thiết lập những thứ như kết nối cơ sở dữ liệu, vị trí file, header/footer HTML, thì ngoài ra gần như không có coupling giữa các màn hình. Khi cần màn hình mới, có thể copy một màn hình cũ rồi sửa, hoặc nhờ LLM tạo màn hình hay endpoint; nếu kết quả tệ thì làm lại
      Ở công ty cũ, tôi từng làm một framework tên Themis để tạo tập huấn luyện ML, dùng microservices, React, Docker, v.v. Nhu cầu thực tế là liên tục thêm tác vụ mới, và với mỗi tác vụ phải nhanh chóng tạo ra một màn hình đơn giản nhưng được tối ưu rất kỹ. Nếu phải đưa ra 20.000 phán đoán thì mỗi lần click một cái đã mệt, còn nếu mỗi lần bốn click thì thành 80.000 lần và bạn sẽ bỏ cuộc
      Với kiến trúc lúc đó, phải viết API endpoint của ứng dụng JAXB và component trong app React nguyên khối, rồi chờ TypeScript, Docker, javac chạy 20 phút. May thì boot được, không thì làm lại từ đầu
      Tôi đã viết bài phê bình Themis và thiết kế Nemesis với mục tiêu phát triển nhanh tác vụ mới, nhưng ở công ty cũ đó là con đường không được chọn. Dù vậy, từ đó đến nay Nemesis và tôi vẫn đang xử lý hàng triệu instance tác vụ
    • Trên laptop của tôi có dán sticker âm dương; bên âm ghi DRY, bên dương ghi YAGNI
    • DRY không nguy hiểm đến thế
      Nó không có nghĩa là phải tách helper chỉ dùng ở một chỗ. Dù logic nằm đầy trong một hàm, class, file, v.v., miễn không bị copy thì vẫn là DRY
      Trừu tượng hóa quá sớm thực sự tồn tại. Việc các lớp CS nhìn chung dạy làm theo hướng đó cũng chẳng giúp gì. Đưa cho người mới một database MySQL, có thể việc đầu tiên họ làm là cố trừu tượng hóa MySQL
    • grug đôi khi dành 100 giờ để làm một cỗ máy hoàn thành công việc, trong khi làm thủ công chỉ mất 1 giờ. Hoặc dành 1 giờ làm cỗ máy rồi mất 100 giờ sửa cái máy ngu ngốc đó
      Dù phức tạp hay đơn giản, nếu kết quả không tạo thêm giá trị thì không quan trọng. Trước hết hãy tập trung tạo thêm nhiều giá trị hơn phần bị bớt đi, rồi sau đó hãy lo về độ phức tạp
  • Một trong những cách tôi thích dùng LLM là đưa bài tiểu luận này vào, rồi yêu cầu nó bình luận về issue hiện tại tôi đang xử lý theo persona grug-brained developer. Rất tốt để xả stress

    • Tôi vẫn chưa quen với LLM, nhưng nghe có vẻ hay. Tôi tò mò “cho nó ăn” bài tiểu luận này nghĩa là làm thế nào
      Có phải chỉ cần bắt đầu prompt kiểu “hãy hành động như Grug Brained Developer trong bài tiểu luận này” không
  • “Độ phức tạp cực kỳ tệ” là một câu hoàn toàn đúng
    Trong suốt những năm làm kỹ thuật phần mềm, suy nghĩ này là một trong số ít nguyên tắc luôn tỏ ra đúng trong mọi tình huống. Một số vấn đề vốn dĩ phức tạp, nhưng ngay cả khi đó, dành thời gian để đi đến giải pháp đơn giản nhất vẫn tốt hơn rất nhiều
    Những công việc hiệu quả nhất của tôi xuất hiện sau khi nghi ngờ cách tiếp cận trước đó và mạnh dạn đơn giản hóa. Có thể mất đi một chút độ linh hoạt tiềm năng, nhưng phần lớn thì mức linh hoạt mà tôi nghĩ là cần thực ra lại không cần đến như vậy
    Ví dụ, từ khi có những LLM khá tốt và hoạt động có tính agent, tôi tránh các kiểu TypeScript quá phức tạp, dễ vỡ và khó debug; thay vào đó viết code giống như đặc tả rồi để LLM sinh tĩnh phần code dựa trên đó
    Phụ thuộc ESLint của dự án liên tục hỏng sau các lần cập nhật phiên bản, nhiều rule không đủ tinh vi để tránh báo sai, và việc duy trì cho đúng với TypeScript và VSCode cũng phức tạp. Chuyển sang Biome.js thì đơn giản hơn và đủ hiệu quả, nhưng gần đây nó cũng bắt đầu có lỗi. Dù vậy, tôi nhận ra linting là thứ có thì tốt, chứ không phải đối tượng đáng để bỏ quá nhiều thời gian chăm sóc, nên đã loại nó khỏi chuỗi công cụ build, và cũng không còn cần bật thường trực trong VSCode nữa. Thỉnh thoảng chạy Biome để kiểm tra style và format code là đủ
    Khi làm một công cụ migration dữ liệu tùy chỉnh cho dự án, tôi thấy migration xuôi là cần thiết, nhưng migration ngược thì không đáng để bỏ thời gian và độ phức tạp ra triển khai. Nếu cần rollback một database có dữ liệu thì cứ khôi phục backup; nếu không có dữ liệu hoặc không phải DB production thì bắt đầu từ trạng thái sạch bằng script khởi tạo có gắn phiên bản là được

    • Hai ví dụ đầu có vẻ chỉ là dùng công cụ khác để che giấu độ phức tạp thì phải
      Tôi cũng không rõ ví dụ thứ ba đơn giản hơn ở chỗ nào. Với tư duy toán học, có thể dễ dàng tạo ra một không gian song ánh. Việc mô phỏng migration ngược bằng phương tiện khác đôi khi còn khó hơn. Tất nhiên còn tùy chi tiết, nên không phải quy tắc chung
    • Nhiều người nói về độ phức tạp, nhưng hiếm ai nói rõ độ phức tạp nghĩa là gì
      Rich thông minh từng nói complect là việc buộc các thứ vào nhau, và tôi đồng ý điểm đó. Rich nói độ phức tạp là xấu, nhưng điểm đó thì tôi không đồng ý. Việc buộc các thứ vào nhau là cần thiết. Nếu chúng không kết nối với nhau thì không thể giải quyết vấn đề
  • Không thể tin được bài này là từ năm 2022
    Tôi tưởng như đã đọc nó 10 năm trước và khi đó đã có thể tự tin nói rằng nó là kinh điển rồi

    • Tôi cũng vậy. Có lẽ nó được truyền cảm hứng từ những công trình sớm hơn
  • Buồn nhưng đúng: lời khuyên sự nghiệp lý tưởng là học nói “yes”, rồi khi thất bại thì học cách đổ lỗi cho grug khác
    Khi mới bước vào thế giới doanh nghiệp, tôi nghĩ điều này không đúng, chỉ là đội kỹ thuật thiếu giao tiếp thôi. Tôi đã sai, và grug đã đúng