Viết mã dễ xóa
- Mỗi dòng mã đều đi kèm chi phí bảo trì. Việc tái sử dụng mã khiến thay đổi trở nên khó khăn hơn.
- Càng nhiều bên sử dụng API, càng có nhiều đoạn mã phải viết lại khi thay đổi.
- Quản lý sự phụ thuộc của mã là một vấn đề quan trọng trong các hệ thống quy mô lớn.
Bước 0: Không viết mã
- Số lượng dòng mã tự nó không cung cấp nhiều thông tin.
- Mã chưa được viết là loại mã dễ xóa nhất.
Bước 1: Sao chép-dán mã
- Mã có thể tái sử dụng về sau có thể được viết dễ hơn thông qua các ví dụ.
- Sao chép-dán mã là một cách để tránh phụ thuộc và đạt được tính linh hoạt.
Bước 2: Đừng sao chép-dán mã
- Nếu mã đã bị sao chép-dán đủ nhiều, đó là lúc trích xuất thành hàm.
- Tạo thư mục
util để lưu nhiều tiện ích khác nhau vào các tệp khác là một cách tốt.
Bước 3: Viết nhiều boilerplate hơn
- Boilerplate tương tự như sao chép-dán mã, nhưng thay đổi mã ở các vị trí khác nhau.
- Boilerplate giúp giảm phụ thuộc và mang lại tính linh hoạt.
Bước 4: Đừng viết boilerplate
- Nếu có quá nhiều boilerplate, nên bọc nó bằng một thư viện có quan điểm rõ ràng về chính sách, workflow và trạng thái.
- Mối quan hệ giữa
requests và urllib3 là một ví dụ điển hình.
Bước 5: Viết những khối mã lớn
- Logic nghiệp vụ được đặc trưng bởi vô số trường hợp ngoại lệ và những bản vá nhanh, bẩn.
- Xóa một sai lầm lớn dễ hơn xóa nhiều sai lầm nhỏ.
Bước 6: Chia mã thành từng mảnh
- Những khối mã lớn có chi phí bảo trì cao.
- Cần tách bạch trách nhiệm của mã và thiết kế mô-đun có tính đến khả năng thay đổi.
Bước 7: Tiếp tục viết mã
- Cần có khả năng viết mã mới độc lập với mã hiện có để có thể thử nghiệm các ý tưởng mới.
- Feature flag là một cách để có thể đổi ý về sau.
Tóm tắt của GN⁺
- Bài viết này giải thích cách tạo ra mã dễ xóa khi viết code.
- Điểm cốt lõi là giảm phụ thuộc của mã, tăng tính linh hoạt và giảm chi phí bảo trì.
- Một dự án có chức năng tương tự là
requests và urllib3.
- Bài viết này nhắc nhở các lập trình viên phần mềm về tầm quan trọng của việc quản lý và bảo trì mã.
1 bình luận
Ý kiến Hacker News
Tôi thích câu "Simple is robust". Nó có nghĩa là hệ thống càng ít phức tạp thì càng dễ thay đổi. Việc lập kế hoạch cho tương lai nên dựa trên mã dễ hiểu hơn là mã có khả năng mở rộng. Ví dụ, chỉ nên trừu tượng hóa khi tình huống thực sự đòi hỏi, chấp nhận một chút lặp lại đơn giản, dùng monolith ở giai đoạn đầu và ưu tiên mở rộng theo chiều dọc hơn là mở rộng theo chiều ngang. Khi xây dựng nhiều hệ thống từ 0 đến 1, tôi nhận ra những điểm chung này.
Tôi ngạc nhiên vì không thấy nhắc đến testing hay observability. Testing có chi phí bảo trì, nhưng nó giúp giảm rủi ro phát sinh vấn đề khi xóa mã. Khi phơi bày một dịch vụ cho caller bên ngoài, cần có một cách mạnh mẽ để đánh dấu một số lệnh gọi là sắp bị ngừng dùng và quan sát xem chúng còn đang được gọi hay không. Gần đây tôi đã bán tự động xóa các GraphQL resolver, và dùng metric về tần suất sử dụng để xác định resolver nào chưa thể xóa. GraphQL có annotation deprecation, nhưng trong dịch vụ thì chúng tôi không xử lý gì đặc biệt. Nếu bổ sung observability để bật cờ khi hàm bị đánh dấu deprecate được gọi, rồi chạy đủ lâu trong production, thì có thể xóa an toàn phần mã đang lộ ra bên ngoài.
Tôi ngày càng cổ vũ cho việc "thiết kế để xóa". Trước đây tôi từng nghĩ mình có thể lên kế hoạch cho mọi tình huống và tạo ra một tác phẩm đáp ứng mọi nhu cầu, nhưng rất khó dự đoán nhu cầu trong tương lai. Rồi sẽ có ngày thứ tôi làm ra trở nên vô dụng với ai đó, và việc họ dỡ bỏ nó là hoàn toàn chính đáng. Vì vậy, ta nên nỗ lực để khiến nó dễ bị loại bỏ. Điều này thường dẫn đến việc giảm coupling, nhưng không giống kiểu các lập trình viên trẻ cố tách mọi thứ thành một framework meta có thể cấu hình được. Đôi khi coupling chặt lại tốt hơn nếu nó giúp dễ hiểu về mặt logic.
Nếu muốn viết mã dễ xóa, bạn nên lặp lại để tránh dependency, chứ không phải lặp lại để quản lý. Hãy chia mã thành các lớp và xây dựng API đơn giản lên trên những phần có thể triển khai đơn giản nhưng dùng thì bất tiện. Hãy tách mã ra, và cô lập những phần khó viết cũng như những phần có khả năng thay đổi cao khỏi phần còn lại. Đừng hardcode mọi lựa chọn; nên cho phép thay đổi một số thứ ở runtime. Theo kinh nghiệm cá nhân của tôi, mã dễ xóa thường được phân lớp và mô-đun hóa tốt, nên cũng dễ mở rộng.
Tôi vẫn thường nói với sinh viên ngành vật lý tính toán rằng phép tính tốt nhất là phép tính mà bạn không cần phải bận tâm đến.
Cá nhân tôi chia mã thành business logic và phần triển khai thực tế. Business logic về bản chất có thể bị lặp lại, nhưng quá nhiều chi tiết kỹ thuật thì không nên lặp lại. Miễn là bạn không xử lý trực tiếp business logic và giữ nó độc lập với ứng dụng, thì business logic có lộn xộn đến đâu cũng được. Nếu có vấn đề và mọi thứ không hoạt động ổn, bạn luôn có lựa chọn xóa toàn bộ phần triển khai, sửa lại nó, và không bị buộc phải lần mò thông số thực tế từ trong phần implementation.
Sai lầm hiển nhiên ở đoạn đầu tiên: vấn đề của việc tái sử dụng mã là nó cản trở bạn đổi ý về sau. Đây nhìn chung là một lập luận sai. Nếu bạn đổi ý và đoạn mã đó đã bị copy-paste ở mười chỗ, thì bạn phải sửa cả mười chỗ. Ngược lại, nếu mã nằm trong một hàm, bạn chỉ cần sửa một lần. Nếu một trong mười chỗ gọi cần hành vi khác đi, bạn vẫn có thể copy ra, hoặc làm cho hàm trở nên tổng quát hơn. Cũng như việc băng qua đường mà không nhìn trước ngó sau, copy-paste gần như luôn là một ý tưởng tồi.
Có một mối tương quan rất rõ: mã tệ thường tồn tại lâu vì nó khó bị loại bỏ.
Tôi tự hỏi liệu ý ở đây có phải là hãy dùng phần mềm càng gần trạng thái mặc định càng tốt, và đừng tùy biến quá sâu hay không.