Vì sao không sao cả nếu bạn không hiểu hoàn toàn codebase
(seangoedecke.com)- Với các hệ thống quy mô lớn lên tới hàng chục triệu dòng, không ai có thể giữ toàn bộ trong đầu, nên kỹ sư cần có khả năng làm việc hiệu quả ngay cả khi chỉ có hiểu biết đúng một phần
- Programming as Theory Building của Peter Naur cho rằng khi sự hiểu biết của đội ngũ hiện tại biến mất thì tốt hơn nên bỏ chương trình và làm lại từ đầu, nhưng các hệ thống lớn gắn với người dùng và vô số ngoại lệ thì khó tái xây dựng từ đầu
- Ngay cả một codebase mà tất cả người phụ trách đã rời đi cũng có thể được hồi sinh nếu trước hết nắm được một luồng từ đầu đến cuối, rồi thận trọng mở rộng phạm vi thay đổi; trong các tổ chức lớn, việc tái xây dựng sự hiểu biết như vậy diễn ra lặp đi lặp lại
- LLM vừa cản trở việc hình thành mô hình tinh thần chi tiết, vừa giúp tạo và tận dụng nhanh hiểu biết cục bộ; cộng tác, yêu cầu pháp lý, cập nhật bảo mật và đưa dependency vào cũng đòi hỏi đánh đổi giữa việc hiểu code và các giá trị khác
- Mô hình tinh thần chính xác giúp phát triển vui hơn và ổn định hơn, nhưng không phải tiêu chuẩn tuyệt đối; trong thực tế, đôi khi cần từ bỏ sự hiểu biết hoàn toàn vì tốc độ, tuân thủ pháp lý và nhu cầu tổ chức
Hiểu biết hoàn toàn và hiểu biết cục bộ
- Với những codebase nhỏ và ít thay đổi thành viên, người ta dễ cho rằng phải hiểu toàn bộ mới có thể làm việc tốt
- Các dự án như Redis hay The Witness) thuộc dạng này
- Với những codebase lớn và thường xuyên thay đổi thành viên như backend tìm kiếm web của Google hay GitHub, không thể hiểu toàn bộ, nên mỗi người làm việc bằng cách nắm tối đa khu vực cục bộ mà mình phụ trách
- Hai môi trường này rất khác nhau về phương pháp phát triển, thông lệ và văn hóa, nhưng trong các thảo luận kỹ thuật phần mềm trên mạng, văn hóa thứ nhất — coi trọng sự hiểu biết hoàn toàn — lại được đại diện quá mức
- Kỹ sư open source có động lực lớn để chia sẻ công việc bằng bài viết, và công việc kỹ thuật thuần túy cũng dễ nổi bật hơn so với trong các hệ thống độc quyền quy mô lớn
- Các hệ thống độc quyền khó công khai vì lý do pháp lý, và ngay cả khi có thể công khai, việc giải thích một codebase lớn đòi hỏi quá nhiều bối cảnh cụ thể
- Trong nhiều môi trường phần mềm, hiểu biết cục bộ không phải là trạng thái sai; với hệ thống lớn, đó là điều tốt nhất có thể đạt được trên thực tế
- Sự khác biệt này dẫn đến vấn đề va chạm giữa văn hóa hiểu biết hoàn toàn và văn hóa hiểu biết cục bộ được phân biệt trong Pure and impure software engineering
Luận điểm của Programming as Theory Building
- Bài luận Programming as Theory Building của Peter Naur xem sản phẩm chính mà lập trình viên tạo ra không phải là code, mà là lý thuyết về chương trình
- Lý thuyết này gồm sự hiểu biết trực giác về việc điều gì xảy ra và vì sao
- Code và tài liệu chỉ có thể chứa một phần sự hiểu biết đó
- Dù mất code, đội ngũ có lý thuyết vẫn có thể viết lại chương trình; nhưng nếu đội ngũ bị thay thế hoàn toàn và mất sự hiểu biết, việc nắm bắt code cũ sẽ trở nên khó khăn
- Theo Naur, không thể tái xây dựng lý thuyết sẵn có chỉ bằng tài liệu hay code, nên cần bỏ chương trình gốc và để đội ngũ mới giải quyết lại vấn đề từ đầu
- The Concept of Mind của Gilbert Ryle, tác phẩm được Naur trích dẫn, nhìn phạm vi xây dựng lý thuyết rộng hơn
- Trong khi thực sự thực hiện một việc gì đó, lý thuyết hoặc bí quyết có thể được tạo ra một cách tự nhiên
- Vì vậy, quá trình khám phá chính code để hiểu một codebase hiện có cũng tương thích với cách tiếp cận này
Vì sao không thể viết lại hệ thống lớn từ đầu
- Một hệ thống đủ lớn đã có người dùng sẽ tích lũy hàng nghìn trường hợp ngoại lệ và hành vi đặc thù khó triển khai lại
- Ngay cả đội ngũ hiểu rõ hệ thống cũng không thể đồng thời xét hết mọi chi tiết, nên khó viết lại toàn bộ trong một lần
- Một cuộc viết lại thành công diễn ra bằng cách chia codebase hiện có thành các phần nhỏ, cô lập rồi thay thế từng phần một
- Rốt cuộc, việc viết lại cũng là công việc áp một chuỗi thay đổi lên hệ thống hiện có
- Nếu không thể thay đổi hệ thống hiện có, việc thay thế hoàn toàn bằng hệ thống mới còn khó hơn nữa
Cách khôi phục một codebase bị bỏ quên
- Ở các công ty công nghệ có hàng trăm triệu dòng code và hàng nghìn kỹ sư, không hiếm khi không còn ai biết một codebase cụ thể
- Chỉ cần vài người phụ trách nghỉ việc vào thời điểm không thuận lợi, hoặc codebase không được bảo trì trong một năm, là có thể rơi vào trạng thái này
- Naur cho rằng gần như sẽ không có chuyện giao việc khôi phục một chương trình đã chết hoàn toàn cho lập trình viên mới mà không có bất kỳ ai biết dù chỉ một phần lý thuyết cũ, nhưng trong các tổ chức lớn, điều đó thực sự xảy ra
- Một codebase bị bỏ quên vẫn có thể được đưa trở lại trạng thái có thể làm việc hiệu quả nếu dành thời gian xây dựng sự hiểu biết mới
- Trước hết, hiểu một luồng xử lý từ đầu đến cuối
- Vừa thay đổi thận trọng, vừa mở rộng phạm vi hiểu biết từ luồng đó sang các khu vực xung quanh
Ai cũng làm việc với lý thuyết không hoàn chỉnh
- Phần mềm quy mô lớn hiện đại đủ lớn đến mức không chỉ cá nhân mà cả đội ngũ cũng không thể giữ mọi hành vi trong đầu
- Trong một codebase đủ lớn, ai cũng làm việc với một lý thuyết ít nhiều không chính xác về chương trình
- Kỹ sư hiệu quả không chờ đến khi có người với hiểu biết hoàn hảo đưa ra câu trả lời, mà đưa ra phán đoán có căn cứ nhất với thông tin hiện có rồi ứng phó với kết quả
- Công việc như vậy đòi hỏi khả năng đưa ra lập trường và sự tự tin ngay cả trong tình huống bất định
- Thực tế rằng không ai biết hoàn toàn toàn bộ hành vi của một sản phẩm quy mô lớn giống với tình huống được bàn trong Nobody knows how software products work
Quy mô code thời Naur và hiện nay
- Vào năm 1985, khi Naur viết bài luận, quy mô chương trình trung bình có lẽ nhỏ hơn ngày nay vài bậc độ lớn
- Ví dụ chương trình lớn đầu tiên mà Naur nêu là một chương trình giám sát công nghiệp khoảng 200.000 dòng, còn ví dụ thứ hai là một compiler
- Phiên bản GCC đầu tiên vào năm 1987 có khoảng 100.000 dòng, nhưng đến năm 2015 đã tăng lên hơn 14 triệu dòng
- Nếu có thể tái sử dụng các test hiện có, một chương trình quy mô 100.000–200.000 dòng có thể được viết lại tương đối dễ, nhưng khó áp dụng cùng nhận định đó cho các hệ thống từ 1–2 triệu dòng trở lên
Hai mặt của LLM và việc xây dựng lý thuyết
- LLM thường bị đánh giá là công cụ không tốt vì cản trở quá trình xây dựng lý thuyết thông thường
- Tuy nhiên, giống như các công cụ phần mềm khác, LLM cũng có hai mặt
- Việc tạo một mô hình tinh thần chi tiết về phần mềm có thể trở nên khó hơn
- Có thể xây dựng lý thuyết cục bộ nhanh chóng
- Có thể hỗ trợ làm việc hiệu quả hơn ngay cả dựa trên hiểu biết không hoàn chỉnh
- Quan hệ giữa LLM và việc hiểu code không thể tóm gọn thành phân biệt ưu/nhược điểm đơn giản; đó vẫn là một đánh đổi phức tạp cần phán đoán
Những lựa chọn cần thiết dù làm code khó hiểu hơn
- Ngoài LLM, còn nhiều yếu tố khác khiến việc duy trì một lý thuyết chính xác về codebase trở nên khó khăn
- Cho phép người khác viết code vào cùng codebase
- Triển khai các tính năng được pháp luật yêu cầu như accessibility hoặc bảo vệ dữ liệu
- Cho phép đồng nghiệp nghỉ việc hoặc chuyển đội
- Nâng cấp phiên bản phần mềm để vá bảo mật
- Đưa thư viện hoặc dependency khác vào
- Không thể đánh giá một công cụ hay thông lệ là xấu chỉ vì nó cản trở việc xây dựng lý thuyết
- Hiểu codebase là một trong nhiều giá trị kỹ thuật, giống như tính dễ đọc, khả năng bảo trì và tính đúng đắn
- Tùy tình huống, có thể ưu tiên hiểu biết chính xác bằng cách hy sinh các giá trị khác
- Ngược lại, cũng có thể nhượng bộ việc hiểu code vì tốc độ, tuân thủ pháp lý hoặc lý do chính trị trong tổ chức
- Phản biện rằng hiểu code là cốt lõi để hiện thực hóa mọi giá trị khác cũng có thể áp dụng y hệt cho tính dễ đọc, khả năng bảo trì và tính đúng đắn; trong thực tế, các giá trị cốt lõi này cũng liên tục được đánh đổi
Sở thích cá nhân và trách nhiệm trong công việc
- Đặc biệt, kỹ sư thuần túy thường thích làm việc trong khi duy trì một mô hình tinh thần chính xác
- Phát triển thú vị hơn và ít căng thẳng hơn
- Cảm giác gần với kỹ thuật thực thụ theo cách họ tự nghĩ hơn
- Lý do nhiều kỹ sư tự làm các dự án open source nhỏ một mình trong thời gian rảnh cũng là vì họ có thể làm việc trong khi duy trì lý thuyết kiểu Naur chính xác về codebase
- Trong công việc, cần tuân theo tập giá trị mà tổ chức trả tiền và yêu cầu, hơn là các giá trị kỹ thuật cá nhân
- Dù coi trọng hiệu năng, bạn vẫn có thể viết code chậm để kịp lịch hoặc đáp ứng các yêu cầu khó
- Việc hiểu hoàn toàn codebase cũng không phải tiêu chuẩn tuyệt đối luôn phải giữ, mà là một lựa chọn có thể trao đổi với các giá trị khác tùy theo mục đích công việc
2 bình luận
Vì thế mới phải bám víu vào sự trừu tượng hóa.
Các ý kiến trên Lobste.rs
Tiêu đề của bài gốc đáng tiếc là khá giống kiểu câu view, còn nội dung thực tế thì gần với ý rằng ngay cả khi chỉ hiểu một phần codebase quy mô lớn, ta vẫn phải có thể tạo ra tiến triển
Nếu nhìn góc độ xây dựng lý thuyết là lý thuyết có cả chiều rộng lẫn chiều sâu, thì nó không mâu thuẫn với luận điểm chính của bài. Chiều rộng chỉ phạm vi nào của hệ thống mà ta hiểu đủ để trả lời câu hỏi và thay đổi thành thạo; chiều sâu chỉ việc ta có thể trả lời những câu hỏi phức tạp đến đâu về một phần cụ thể và thực hiện các thay đổi phức tạp trong khi vẫn giữ được tính toàn vẹn
Nếu viết lại mã theo từng mô-đun và có giai đoạn chuyển đổi, tức là ta đang xây dựng một lý thuyết hẹp nhưng sâu về mô-đun đó rồi làm việc dựa trên nó. Với một hàm nhỏ thì thêm một tham số hay một giá trị trả về có thể vẫn ổn, nhưng khi quy mô lớn hơn, việc bắt đầu lại từ trạng thái sạch dù chỉ với một mô-đun cụ thể, rồi nối các phần khác để gọi implementation mới, là hợp lý hơn. Về căn bản, cách cố gắng cải thiện dần độ đúng đắn của một implementation vốn có nhiều lỗi thường không hiệu quả, và điều này cũng hay đúng với các thuộc tính như hiệu năng
Tuy nhiên, khi Naur nói rằng “thay vì hồi sinh một chương trình, nên loại bỏ văn bản chương trình hiện có và để một đội lập trình viên mới giải quyết bài toán đã cho từ đầu”, vẫn còn có thể tranh luận rằng ông muốn nói phải xóa mã cũ trước khi bắt đầu viết lại, hay có thể loại bỏ nó sau khi đã xác nhận chương trình mới là một phương án thay thế phù hợp
Hơn nữa, trong thực tế, bản thân bài toán đã cho lại không hề được cho sẵn. Naur giả định rằng định nghĩa bài toán ban đầu vẫn còn, nhưng trong các codebase lớn thì thường không có. Thứ gần nhất thường là bộ test, nên ta có thể thay thế các thành phần trong khi vẫn tiếp tục cho test pass
Thời đó cũng chưa có continuous deployment. Ngày nay, khi dịch vụ vẫn đang chạy, ta phải trực on-call, trả lời câu hỏi và sửa lỗi khẩn cấp, nên phép tính lời-lỗ quanh việc viết lại cũng khác đi
Phản biện này có vài chỗ diễn đạt tốt, nhưng phần lớn lại có vẻ hiển nhiên. Trong một hệ thống đủ lớn, hiểu một phần là trạng thái duy nhất, nhưng tổ chức luôn nên theo đuổi mức hiểu sâu hơn
Bỏ qua lập luận “được trả tiền để làm việc”, phần mềm quy mô lớn thường không làm hài lòng người dùng hoặc không đạt mục tiêu kinh doanh. Có thể xem Windows và OS X cũng đang mục ruỗng vì độ phức tạp tích tụ
Ban lãnh đạo, nếu có thể làm độ phức tạp biến mất bằng phép màu, chắc cũng sẽ làm vậy. Chỉ là đây vẫn là một vấn đề kỹ nghệ phần mềm chưa được giải quyết; thay vì nói ban lãnh đạo muốn phát hành mã tệ, nên hiểu là họ đã chấp nhận sự đánh đổi đó trong điều kiện hiện tại
Cũng có thể cho rằng “nếu đã độc quyền thì đủ rồi, cứ tung ra phần mềm kém và đầy lỗi cũng được”, nhưng như vậy là quá hư vô. Có nhiều công ty phần mềm không độc quyền hoặc không thể giữ chân người dùng, nên ở những nơi đó chất lượng thực sự quan trọng
Theo cách diễn giải này, khái niệm hiểu biết cũng trông giống một dạng gác cổng
Cách tiếp cận được mô tả trong bài có liên hệ chặt chẽ với khả năng suy luận cục bộ về một phần mã
Suy luận cục bộ, tức là hiểu một phần của codebase lớn mà không cần hiểu toàn bộ xung quanh, là một khái niệm cốt lõi mà khoa học máy tính đã theo đuổi ngay từ đầu
Lợi ích chính của lập trình có cấu trúc là cho phép suy luận cục bộ, và khuyến nghị tránh biến toàn cục nhìn chung cũng xuất phát từ cùng mục đích. Lập trình hàm cho phép suy luận cục bộ bằng cách loại bỏ hiệu ứng phụ, còn cách kết hợp cấu trúc dữ liệu với mã xử lý chúng, một trong các nguyên tắc của lập trình hướng đối tượng, cũng cung cấp một phương tiện khác để suy luận cục bộ
Sức mạnh thật sự của những khái niệm nền tảng này nằm ở chỗ, dù đó không phải tình huống lý tưởng, chúng cho phép ta suy luận và làm việc trên những phần nhỏ mà không cần hiểu toàn bộ codebase
Tôi đọc bài này như một phản biện thú vị đối với lời khuyên “phải hiểu mọi dòng trong codebase”, và vì luận điểm trung tâm của bài gắn với thực hành phát triển nên tôi đã gắn thẻ
#practicesTôi biết cộng đồng này không thích LLM, nhưng tôi nghi ngờ liệu bài này có thực sự thuộc
#vibecodinghay khôngvibecoding; tôi không đồng ý, nhưng đó là thông lệ. Câu hỏi cốt lõi của bài này cũng liên quan chặt chẽ đến lập trình có LLM hỗ trợTuy nhiên, tôi đồng ý rằng
vibecodingkhông nên là thẻ duy nhất. Bài viết chỉ đến cuối mới nói rõ về LLM, còn luận điểm chính thì thú vị và đáng thảo luận cả trong thời kỳ trước LLM. Vì vậy tôi cho rằng có đủ cơ sở để khôi phục thẻpractices, nên đã tự khôi phục, và khuyến nghị người khác cũng làm như vậyNó vẫn liên quan ngay cả trong một thế giới nơi việc dùng LLM có thể cản trở sự hiểu biết đầy đủ về codebase
Nếu thẻ này có nghĩa là “có thể liên quan đến người quan tâm đến vibe coding” thì chắc chắn là phù hợp. Ngược lại, nếu nó có nghĩa là “không liên quan trừ những người quan tâm đến vibe coding” thì không phù hợp. Tất cả các thẻ khác đều được dùng theo nghĩa trước, nhưng khá nhiều người dùng Lobsters lại chỉ dùng riêng thẻ này theo nghĩa sau, nên rốt cuộc họ chắc chắn sẽ bỏ lỡ bài này
Một đặc điểm khác của phát triển hiện đại, khác với năm 1985, là ngay cả khi bạn trở thành chuyên gia về một codebase 300.000 dòng, tuần sau bạn vẫn có thể phải làm việc trên một codebase 300.000 dòng hoàn toàn khác
Các lập trình viên chuyên nghiệp có kinh nghiệm đã quen với việc có thể bị ném vào vùng chưa biết bất cứ lúc nào, và dựa vào sự kết hợp giữa các idiom chung (Google C++ Style Guide), các công cụ tự động như compiler báo lỗi khi dùng sai API, và trực giác về việc một chương trình lớn “nên được tổ chức như thế nào”
Tôi cũng đã thấy các cuộc bàn luận kiểu “giờ không còn ai hiểu toàn bộ hành vi của codebase nữa thì làm sao tiếp tục phát triển được?”, như với Bun được viết lại bằng LLM, nhưng chuyện đó cũng giống như hỏi nếu không phải tác giả thì làm sao hiểu được một cuốn tiểu thuyết. Dù sao nó cũng là mã, nên cứ đọc là được. Nếu không hiểu một hàm nào đó, bạn có thể chia nó thành nhiều mảnh, thử viết test, vẽ luồng điều khiển ra giấy, hay làm bất cứ điều gì
Vì bạn sẽ thường xuyên chuyển qua lại giữa các dự án, cũng không có lý do để tiếp tục duy trì hiểu biết về codebase cũ. Đến một lúc nào đó mọi thứ đều hòa lẫn na ná nhau. Ngay cả khi phải giúp mã Scala của một startup được công ty mua lại giao tiếp với một dịch vụ Ruby dựa trên JSON đang được viết lại bằng Go thông qua một giao thức RPC tùy chỉnh dựa trên Thrift, chỉ cần tìm kiếm
Scala syntax referencevàThrift wire encodingkhoảng một tiếng là có thể bắt đầuCũng không cần phải trở thành chuyên gia. Vì một tháng sau bạn có thể đang debug xem tại sao một type checker dựa trên OCaml cho JavaScript lại crash trong một implementation Linux kernel bằng Go. Rốt cuộc tất cả cũng chỉ là mã