Viết code dễ xóa, không phải code khó mở rộng (2016)
(programmingisterrible.com)- Ngay từ thời điểm được viết ra, code đã phát sinh chi phí bảo trì, nên trong nhiều trường hợp cấu trúc dễ xóa hoặc thay thế về sau còn quan trọng hơn tính tái sử dụng
- Càng nhiều người dùng API, chi phí thay đổi càng lớn; và phụ thuộc càng sâu vào API bên thứ ba thì codebase càng bị ảnh hưởng mạnh khi bên ngoài thay đổi
- Trùng lặp, boilerplate, layering, các khối code lớn, tách mô-đun và feature flag đều có thể trở thành công cụ quản lý phụ thuộc tùy theo tình huống
- Sự phân tách tốt không hẳn là gom các chức năng chung lại, mà gần hơn với việc che giấu khỏi nhau những quyết định thiết kế khó thay đổi hoặc có khả năng sẽ thay đổi
- Code tốt không phải là code hoàn hảo ngay từ đầu, mà là legacy code ít gây cản trở theo thời gian, và cuối cùng là code dễ xóa
Code là chi phí, và xóa code là giảm chi phí
- Mọi đoạn code, ngay từ lúc được viết, đều tạo ra chi phí bảo trì; tái sử dụng có thể giảm lượng code nhưng cũng có thể khiến việc đổi ý về sau trở nên khó hơn
- Càng nhiều code sử dụng một API thì mỗi thay đổi của API đó càng kéo theo nhiều chi phí viết lại
- Càng phụ thuộc nhiều vào API bên thứ ba thì tác động khi API đó thay đổi càng lớn
- Trong các hệ thống quy mô lớn, việc code khớp nối với nhau ra sao và phần nào phụ thuộc vào phần nào trở thành vấn đề ngày càng khó theo thời gian
- Nếu xem số dòng code không phải là “số dòng tạo ra” mà là “chi phí đã bỏ ra”, thì việc xóa code chính là hạ thấp chi phí bảo trì
- Mục tiêu không chỉ là tạo ra phần mềm có thể tái sử dụng, mà là tạo ra phần mềm có thể vứt bỏ
Bước 0: đừng viết code
- Số dòng code tự nó không nói lên tất cả, nhưng quy mô như 50 dòng, 500 dòng, 5.000 dòng, 10.000 dòng hay 25.000 dòng vẫn rất quan trọng
- Một monolith 1 triệu dòng sẽ cần nhiều thời gian, chi phí và công sức hơn để thay thế so với monolith 10.000 dòng
- Code càng nhiều thì càng khó loại bỏ, nhưng chỉ giảm bớt một dòng code thì hầu như không tiết kiệm được gì
- Loại code dễ xóa nhất là code ngay từ đầu đã không được viết
Bước 1: copy-paste
- Code có thể tái sử dụng thường dễ làm hơn nếu được tạo ra sau khi đã xuất hiện nhiều trường hợp sử dụng thực tế, thay vì đoán trước nơi sẽ dùng trong tương lai rồi viết sẵn
- Thử copy-paste vài lần trong codebase giúp hiểu rõ cách dùng thực tế
- Ngay khoảnh khắc một đoạn code được biến thành API dùng chung, nó sẽ trở nên khó thay đổi hơn
- Code gọi hàm sẽ phụ thuộc không chỉ vào hành vi được tài liệu hóa mà còn vào cả những hành vi có chủ đích hoặc vô tình quan sát được từ cách triển khai
- Xóa code bên trong một hàm đơn giản hơn xóa chính bản thân hàm đó
Bước 2: ngừng copy-paste
- Khi một đoạn code đã lặp lại đủ nhiều, đó là lúc nên kéo nó lên thành hàm
- Ví dụ như code mở file cấu hình rồi trả về bảng băm, hay code xóa thư mục: những đoạn tiện ích thường cần ở phía trên thư viện chuẩn
- Nên để
utildưới dạng thư mục thay vì một file duy nhất, và đặt các tiện ích khác nhau ở các file khác nhau- Một file
utilduy nhất sẽ cứ phình ra mãi, và khi đã quá to thì rất khó tách nhỏ
- Một file
- Những đoạn code ít đặc thù với ứng dụng hoặc dự án thì dễ tái sử dụng hơn và ít có khả năng bị thay đổi hoặc bị xóa hơn
- Ví dụ như code thư viện xử lý logging, API bên thứ ba, file handle, process
- List, bảng băm và collection khó bị xóa không chỉ vì có giao diện đơn giản mà còn vì phạm vi của chúng không có xu hướng phình to theo thời gian
- Cốt lõi là đặt những phần khó xóa càng xa càng tốt khỏi những phần dễ xóa
Bước 3: viết nhiều boilerplate hơn
- Tạo thư viện có thể tránh được copy-paste, nhưng trên thực tế lại thường phải viết rất nhiều boilerplate để dùng thư viện đó
- Boilerplate giống copy-paste ở chỗ mỗi lần chỉ thay đổi đôi chút tại những vị trí khác nhau
- Kiểu trùng lặp này là cách chấp nhận sự dài dòng để đổi lấy ít phụ thuộc hơn và linh hoạt hơn
- Những thư viện cần boilerplate thường là trường hợp khó trộn lẫn policy và protocol, như network protocol, wire format hoặc công cụ parsing
- Protocol nói về những gì chương trình có thể làm
- Policy nói về những gì chương trình nên làm
- Loại code này thường khó xóa vì nó là yêu cầu để giao tiếp với máy tính khác hoặc xử lý file khác
- Điều quan trọng là không rải business logic vào những đoạn code như vậy
- Dù phải viết nhiều dòng hơn, vẫn tốt hơn nếu những dòng đó nằm ở phần dễ xóa
Bước 4: đừng viết boilerplate
- Khi boilerplate trở nên quá nhiều, đó là lúc nên tạo thư viện bọc quanh thư viện linh hoạt bên dưới nhưng có quan điểm rõ ràng về policy, workflow và state
- Tạo ra API dễ dùng gần như là thư viện hóa phần boilerplate
- Python HTTP client
requestslà ví dụ về việc cung cấp giao diện đơn giản bên trênurllib3vốn dài dòng hơnrequestsxử lý workflow phổ biến khi dùng HTTP và che đi nhiều chi tiết thực dụngurllib3cung cấp pipelining, quản lý kết nối, v.v. và không che các chi tiết đó khỏi người dùng
- Bọc một thư viện bằng thư viện khác không chỉ là che chi tiết mà còn là tách biệt mối quan tâm
- Trong thư mục
util, không nên đặt business logic; tốt hơn là xếp chồng thư viện dễ dùng lên trên thư viện đơn giản để triển khai - Đôi khi cũng nên bọc cả thư viện bên thứ ba
- Nhờ vậy, toàn bộ dự án không bị khóa chặt vào một lựa chọn cụ thể, và có thể tạo thư viện phù hợp với code của chính mình
- API dễ dùng và API dễ mở rộng thường xung đột với nhau
- Layering không hẳn là viết code để sau này xóa đi, mà gần hơn với việc làm cho phần code khó xóa trở nên dễ dùng mà không làm bẩn business logic
Bước 5: viết các khối code lớn
- Dù có copy-paste, refactor, layering hay composition thì cuối cùng code vẫn phải làm một việc gì đó, nên đôi khi cần một khối code lớn giữ phần còn lại lại với nhau
- Business logic có thể được đặc trưng bởi vô số edge case và những bản vá nhanh
- Code game hay code của founder cũng có thể được xem là cùng một loại code chọn đường tắt để tiết kiệm đáng kể thời gian
- Đôi khi xóa một sai lầm lớn còn dễ hơn loại bỏ 18 sai lầm nhỏ đan xen vào nhau
- Vì nhiều công việc lập trình mang tính khám phá, nên làm sai vài lần rồi lặp lại có thể nhanh hơn là cố đúng ngay từ đầu
- Khi làm game đầu tiên, đừng bắt đầu bằng việc viết engine; trước khi viết ứng dụng cũng đừng vội làm web framework
- Monorepo cũng là một kiểu đánh đổi tương tự
- Rất khó biết trước nên chia code thế nào, và một sai lầm lớn thường dễ triển khai hơn 20 sai lầm gắn chặt với nhau
- Nếu biết rằng code đó sớm muộn sẽ bị bỏ, bị xóa hoặc dễ thay thế, ta có thể chấp nhận nhiều đường tắt hơn
- Mục tiêu không phải là lặp lại cùng một cục bùn mười lần để hoàn thiện sai lầm cũ, mà là mỗi lần tạo ra sai lầm mới, chấp nhận rủi ro mới và tích lũy qua lặp lại
- Rốt cuộc dự án либо thất bại либо trở thành legacy code, và thất bại xảy ra thường xuyên hơn thành công
- Xóa toàn bộ thường dễ hơn xóa code từng mảnh
Bước 6: chia code thành từng phần
- Cục bùn lớn là thứ dễ tạo nhất nhưng cũng có chi phí bảo trì cao nhất
- Một thay đổi tưởng như đơn giản có thể khiến gần như mọi phần trong codebase đều bị đụng tới bằng giải pháp tạm bợ
- Loại code từng dễ xóa khi xóa cả khối lại trở nên khó xóa khi phải xóa từng phần
- Mô-đun nên được chia không phải theo chức năng chung, mà theo những gì không chia sẻ với phần còn lại và những quyết định thiết kế cần được che giấu
- Theo tiêu chí của D. Parnas, có thể liệt kê các quyết định thiết kế khó hoặc dễ thay đổi, rồi thiết kế sao cho mỗi mô-đun che giấu các quyết định đó khỏi mô-đun khác
- Mô-đun không được tạo ra vì mục đích tái sử dụng, mà vì khả năng thay đổi
- Nguyên tắc trách nhiệm đơn lẻ có thể hiểu là “mỗi mô-đun chỉ nên xử lý một vấn đề khó”, nhưng quan trọng hơn là “mỗi vấn đề khó chỉ nên được xử lý trong một mô-đun”
- Nếu một mô-đun làm hai việc, thường là vì muốn thay đổi một phần thì lại phải thay đổi cả phần kia
- Một component kinh khủng nhưng có giao diện đơn giản đôi khi còn dễ hơn hai component cần phối hợp hết sức cẩn thận
Kết nối lỏng và giao diện chung
- Một hệ thống có thể xóa một phần mà không cần viết lại phần khác thường được gọi là kết nối lỏng
- Kết nối lỏng gần với trạng thái mà khi đổi ý, ta không phải sửa quá nhiều code
- Ngay cả việc hardcode một biến một lần, hoặc dùng cờ dòng lệnh thay vì biến, trong một số trường hợp cũng có thể là kết nối lỏng
- Microsoft Windows đạt mục tiêu này bằng cách tách API bên ngoài và API nội bộ
- API bên ngoài gắn với vòng đời của chương trình desktop
- API nội bộ gắn với kernel nền tảng
- Việc che giấu API giúp có được sự linh hoạt mà không phá vỡ nhiều phần mềm
- HTTP cũng là một ví dụ về kết nối lỏng
- Có thể đặt cache phía trước HTTP server
- Có thể chuyển ảnh sang CDN và chỉ đổi link mà không làm hỏng trình duyệt
- HTTP error code gán mã riêng cho các vấn đề phổ biến để client có thể tự xử lý nhiều lỗi
- Cách xử lý lỗi cũng nên được tính cùng lúc khi chia code thành các phần nhỏ
Xử lý lỗi và độ kết dính
- Erlang/OTP dùng một cách khá đặc biệt để xử lý lỗi: cây giám sát
- Mỗi process trong hệ thống Erlang thường được supervisor khởi động và giám sát
- Khi process có vấn đề, nó sẽ kết thúc
- Khi process kết thúc, supervisor sẽ khởi động lại nó
- Supervisor được bootstrap process khởi động; nếu supervisor gặp lỗi, bootstrap process sẽ khởi động lại nó
- Ý tưởng cốt lõi là thất bại nhanh và khởi động lại thường nhanh hơn so với cố xử lý lỗi
- Lỗi tạm thời đôi khi có thể được dập bằng cách tắt đi bật lại
- Xử lý lỗi và phục hồi nên được đặt ở lớp ngoài cùng của codebase; điều này được biết đến như end-to-end principle
- Xử lý lỗi ở hai đầu thường dễ hơn ở giữa đường kết nối, và dù có xử lý ở bên trong thì cuối cùng vẫn cần kiểm tra ở mức cao nhất
- Xử lý lỗi là một trong nhiều cách khiến hệ thống bị gắn chặt với nhau
IMAP, file system, SQL, middleware
- IMAP là một dạng ngoại lệ nơi gần như mọi thao tác đều có tùy chọn và cách xử lý riêng, khiến xử lý lỗi trở nên đau đớn
- Trong IMAP, lỗi có thể xuất hiện giữa chừng trong kết quả của thao tác khác
- Thay vì UUID, nó tạo token riêng để nhận diện từng thư, và token này cũng có thể thay đổi ngay giữa kết quả thao tác
- Nhiều thao tác IMAP không có tính nguyên tử
- Phải mất hơn 25 năm mới xuất hiện cách di chuyển email ổn định từ thư mục này sang thư mục khác
- Nó cũng có mã hóa UTF-7 đặc biệt và một kiểu base64 riêng
- File system và database là các ví dụ so sánh tốt hơn cho lưu trữ từ xa
- File system có một tập thao tác cố định và nhiều đối tượng
- SQL trông như giao diện rộng hơn file system, nhưng vẫn đi theo mẫu nhiều thao tác trên tập hợp và nhiều hàng
- Không phải lúc nào cũng có thể thay database cho nhau, nhưng vẫn dễ tìm thứ hoạt động với SQL hơn là với một ngôn ngữ truy vấn tự chế
- Finagle của Twitter dùng API chung cho service, giúp dễ thêm xử lý timeout, cơ chế retry và kiểm tra xác thực vào code client và server
- Một ví dụ tốt về kết nối lỏng thường cũng là ví dụ về giao diện đồng nhất
- Một codebase khỏe mạnh không nhất thiết phải được mô-đun hóa hoàn hảo, nhưng cần có đủ khoảng cách giữa các phần đang chuyển động
- Code kết nối lỏng không nhất thiết luôn dễ xóa, nhưng chắc chắn dễ thay thế và thay đổi hơn nhiều
Bước 7: tiếp tục viết code
- Nếu có thể viết code mới mà không phải xử lý code cũ, việc thử nghiệm ý tưởng mới sẽ dễ hơn rất nhiều
- Cốt lõi không phải là microservice hay monolith, mà là trong lúc còn đang khám phá mình muốn làm gì, vẫn có thể đặt một hai thử nghiệm lên trên hệ thống
- Feature flag là một cách để có thể đổi ý về sau
- Feature flag không chỉ phục vụ thử nghiệm tính năng mà còn cho phép triển khai thay đổi mà không cần deploy lại phần mềm
- Google Chrome nhận ra rằng phần khó nhất trong chu kỳ phát hành định kỳ là thời gian cần để hợp nhất các feature branch tồn tại quá lâu
- Nếu có thể bật tắt code mới mà không cần biên dịch lại, ta có thể chia thay đổi lớn thành các lần merge nhỏ và tránh ảnh hưởng đến code hiện có
- Khi tính năng mới xuất hiện sớm hơn trong cùng một codebase, sẽ dễ nhìn rõ hơn tác động của việc phát triển tính năng dài hạn lên các phần khác
- Feature flag không chỉ là công tắc dòng lệnh đơn giản, mà là cách tách phát hành tính năng khỏi việc merge nhánh và deploy code
- Khi việc triển khai phần mềm mới có thể mất hàng giờ, hàng ngày hoặc hàng tuần, khả năng đổi ý lúc runtime càng trở nên quan trọng
Code tốt là legacy code không gây cản trở
- Quan trọng hơn bản thân việc lặp lại là có được vòng phản hồi
- Thay vì tạo mô-đun để tái sử dụng, điều cốt lõi là cô lập component để phục vụ thay đổi
- Khả năng thích ứng với thay đổi không chỉ bao gồm phát triển tính năng mới mà còn cả việc loại bỏ tính năng cũ
- Viết code dễ mở rộng là đặt cược rằng sau 3 tháng lựa chọn ban đầu của mình vẫn là lựa chọn đúng
- Code có thể xóa được bắt đầu từ giả định ngược lại
- Layering, cô lập, giao diện chung và composition không phải là bản thân phần mềm tốt, mà là cách tạo ra phần mềm có thể thay đổi theo thời gian
- Không cần vứt bỏ mọi thứ, nhưng một phần thì phải xóa đi
- Code tốt không phải là code đúng ngay từ đầu, mà là legacy code không gây cản trở
- Code tốt là code dễ xóa
1 bình luận
Các ý kiến trên Hacker News
Câu tôi thích là đơn giản là vững chắc
Tương tự như định luật thay đổi liên tục của Lehman, điều đó có nghĩa là hệ thống càng ít phức tạp thì càng dễ thay đổi
Tôi nghĩ thay vì viết code có khả năng mở rộng để chuẩn bị cho tương lai, tốt hơn là chuẩn bị cho tương lai bằng code trực quan
Ví dụ như chỉ trừu tượng hóa khi thật sự cần, chấp nhận một chút lặp đơn giản, ban đầu bắt đầu bằng monolith, và ưu tiên mở rộng theo chiều dọc trước khi mở rộng theo chiều ngang
Tôi đã từng xây dựng nhiều hệ thống từ 0→1, và luồng chung của tất cả đều theo hướng này
https://en.m.wikipedia.org/wiki/Lehman%27s_laws_of_software_...
Không xử lý các trường hợp biên không làm code trở nên vững chắc hơn, dù nó trông có đơn giản hơn đến đâu đi nữa
Nó mở ra tranh luận về “đơn giản” là gì và áp dụng vào hệ thống như thế nào, mà đây cũng là một câu hỏi phức tạp đến mức Rich Hickey đã từng bàn đến
Có lẽ “ngu ngốc thì vững chắc” hoặc “thẳng thắn thì vững chắc” có thể truyền đạt ý đồ tốt hơn
Chỉ cần viết code làm đúng việc cần làm. Đừng tạo ra các vấn đề mở rộng giả định, đừng tạo ra các abstraction khôn khéo để trông thông minh, cứ viết monolith rồi đưa lên VM là có thể chạy production ngay
Khi có vấn đề thì giải quyết lúc đó, và nếu có thể thì tốt nhất là sau khi dòng tiền đã dương
Một startup “AirBnb cho chó” với 0 người dùng thì lo C100K làm gì? AWS thuyết phục bạn trả tiền cho serverless là vì lợi ích của bạn, hay vì muốn moi tiền của bạn?
Các bài liên quan:
Write code that is easy to delete, not easy to extend (2016) - https://news.ycombinator.com/item?id=24989351 - Tháng 11/2020 (30 bình luận)
Write code that is easy to delete, not easy to extend (2016) - https://news.ycombinator.com/item?id=23914486 - Tháng 7/2020 (109 bình luận)
Write code that is easy to delete, not easy to extend - https://news.ycombinator.com/item?id=18761739 - Tháng 12/2018 (2 bình luận)
Write code that is easy to delete, not easy to extend - https://news.ycombinator.com/item?id=11093733 - Tháng 2/2016 (133 bình luận)
Tóm tắt ngắn gọn những sai lầm thời trẻ của tôi: giờ tôi lại tin vào thiết kế để xóa bỏ
Trước đây tôi từng nghĩ mình có thể dự đoán mọi tình huống và tạo ra một tác phẩm nghệ thuật tuyệt vời đáp ứng mọi yêu cầu. Nhưng không ai dự đoán được yêu cầu tương lai giỏi đến thế
Một ngày nào đó, thứ tôi tạo ra sẽ trở thành “cái thứ ngu ngốc đó” đối với ai đó, và dù hiện giờ tôi tự hào về nó đến đâu, việc họ phá bỏ tất cả có thể là chính đáng
Vì vậy tốt hơn là dồn sức vào việc làm cho nó dễ gỡ bỏ. Điều này thường làm giảm coupling, nhưng điểm quan trọng là nó khác với kiểu tách coupling của một lập trình viên trẻ đầy nhiệt huyết muốn tách mọi thứ thành một framework có thể cấu hình meta
Đôi khi coupling chặt nhưng dễ hiểu lại tốt hơn
https://news.ycombinator.com/item?id=41219130
Ví dụ sẽ xuất hiện những thứ như CommonExcelFileParser, CommonExcelFileParserUtilities, HasExcelParseStatus, ProductImportExcelParser, ProductImportExcelParserView, ProductImportExcelParserResultHandler, rồi chúng trở thành nền tảng của code xung quanh
Nó giống như khi bắt đầu một dự án frontend bằng React hay Angular, việc chuyển sang thứ khác trở thành lao động kiểu Sisyphus
Thực tế là người ta tạo ra cả một nền tảng, và dù có những lựa chọn sẽ gây vấn đề trong tương lai, vì coupling mà việc refactor khó hơn nhiều so với một codebase trừu tượng hóa chưa đủ
Có vẻ mọi người thích làm những việc này hơn là áp dụng KISS và YAGNI để tạo code dễ xóa, nên tôi không biết phải làm gì trong những trường hợp đó
Yêu cầu kinh doanh thay đổi và dịch chuyển, nên đừng cố dự đoán; hãy viết những thứ dễ thay thế hoặc vứt bỏ
Framework và thư viện thì hơi khác. Chúng vẫn phải thích nghi với thay đổi của thế giới, nhưng có thể làm với nhịp độ ôn hòa hơn nhiều
Vấn đề lớn nhất là trong các ứng dụng nghiệp vụ vốn đã dùng framework như Rails hay Asp.Net, các lập trình viên lại muốn tạo thêm “framework” nữa
Nếu bạn không viết Linux kernel thì đừng viết như Linux kernel
Việc bài viết này hoàn toàn không đề cập đến kiểm thử và khả năng quan sát là khá lạ
Kiểm thử cũng có chi phí bảo trì, nhưng nó giúp giảm nguy cơ làm hỏng thứ gì đó khi ta loại bỏ một phần nào đó
Hơn nữa, nếu đã phơi bày dịch vụ cho các bên gọi bên ngoài, bạn cần cả một cách vững chắc để đánh dấu một số lời gọi là sắp bị loại bỏ rồi xóa sau, lẫn một cách quan sát xem chúng còn được gọi không và ai đang gọi chúng
Gần đây tôi lần đầu bán tự động gỡ bỏ một GraphQL resolver đã được phơi bày, và vì đã có sẵn chỉ số về tần suất sử dụng của từng resolver cụ thể nên tôi đã phân tích chúng để lấy danh sách các resolver không thể xóa
GraphQL vốn đã có annotation deprecated, nhưng dịch vụ của chúng tôi không xử lý annotation đó theo cách đặc biệt nào
Vì vậy chúng tôi đã thêm khả năng quan sát để đánh dấu khi một hàm deprecated được gọi, cho chạy đủ lâu trên môi trường production, rồi mới có thể xóa an toàn phần mã đã phơi bày ra bên ngoài
Khi bắt đầu làm mọi thứ phức tạp quá mức, mọi thứ sẽ trở thành một mớ rối liên kết chằng chịt, và lập trình viên không còn biết thay đổi sẽ gây ảnh hưởng gì
Dĩ nhiên có nhiều cách để làm hỏng. Bạn có thể làm theo các nguyên tắc “best practice” ngớ ngẩn, hoặc làm “microservices” theo kiểu không biết ai đang tiêu thụ dịch vụ nào. Nhưng như vậy thì không phải là tạo ra thứ dễ xóa
Việc tiêu thụ từ bên ngoài là một ví dụ hay. Cảnh báo hợp lý cho người tiêu thụ về việc ngừng dịch vụ là điều chính đáng, nhưng nếu bạn không thể thực sự tắt nó khi muốn, thì đó không phải là hệ thống được thiết kế để dễ xóa
Nếu cách đó phù hợp thì cứ làm như vậy. Chỉ là kỳ vọng rằng kiểm thử và quan sát sẽ cho biết nó có bị hỏng hay không thì có khả năng sẽ không hiệu quả
Tôi không phản đối bản thân kiểm thử, nhưng khó có thể xem nó là một cơ chế bảo vệ cực kỳ tốt để cho biết bạn có làm hỏng gì trong một chuỗi dài và phức tạp hay không. Vì trên thực tế, đạt được phạm vi kiểm thử đủ để thật sự bảo vệ cũng rất khó
Nếu xóa một phần code, bạn cũng có thể xóa một phần test
Có thể xem như bài viết chỉ nói về code, còn tác động liên quan đến test được bao hàm ngầm
Không thể giả định rằng vì bài viết không nói đến test nên nghĩa là không nên viết test
Nhìn phần này thì tôi có cảm giác tiêu đề không phải lúc nào cũng đúng: code dễ xóa thường cũng là code dễ mở rộng
Vì nó được phân tầng, có tính mô-đun, và cô lập các mảnh khác nhau thông qua các trừu tượng như interface hoặc các hợp đồng kiểu khác
Tôi vẫn hay nói với sinh viên vật lý tính toán rằng phép tính tốt nhất là phép tính không cần phải thực hiện
Cá nhân tôi chia code thành hai phần: business logic và phần hiện thực thực tế
Business logic về bản chất có thể bị lặp, nhưng các chi tiết kỹ thuật thì không nên bị lặp quá nhiều
Phần hiện thực thực tế có thể lộn xộn đến đâu cũng được, miễn là nó không trực tiếp chứa business logic và được giữ độc lập với ứng dụng
Làm như vậy, khi nhận ra có gì đó rối rắm và vận hành không ổn, bạn sẽ có lựa chọn xóa toàn bộ phần hiện thực, thay vì phải lần ngược đặc tả thực từ phần hiện thực rồi cố sửa gượng ép
Câu ở đoạn đầu rằng “vấn đề của tái sử dụng code là nó cản trở việc đổi ý về sau” rõ ràng là một sai lầm
Nói chung thì điều đó sai. Nếu bạn đổi ý mà code đã bị copy-paste ở mười chỗ, bạn phải sửa cả mười chỗ
Ngược lại, nếu nó nằm trong một hàm thì chỉ cần đổi một lần. Ngay cả khi sau đó phát hiện một trong mười lời gọi không nên thay đổi, lúc ấy bạn có thể copy-paste hoặc tổng quát hóa hàm hơn
Giống như băng qua đường mà không nhìn, copy-paste hầu như luôn là một ý tưởng tồi
Nhưng trừu tượng tệ thì dẫn đến nhiều tháng trả nợ kỹ thuật
Dĩ nhiên câu trả lời là “đừng tạo ra trừu tượng tệ”, nhưng ai cũng biết điều đó diễn ra thế nào trong một đội ngũ và giữa các yêu cầu sản phẩm luôn thay đổi
Chúng tôi có một git submodule chứa các widget UI dùng chung, và giờ việc thay đổi một trong số chúng gần như là bất khả thi, nên sao chép component vào trong project rồi sửa cục bộ còn dễ hơn
Đây là vấn đề. Code dùng chung nên được giữ ở mức tối thiểu nhất có thể, và bản thân việc dùng chung khiến thay đổi trở nên khó khăn
Nếu mọi thứ nằm trong một hàm, phần lớn lập trình viên sẽ cố sửa hàm đó sao cho thỏa mãn cả 10 trường hợp. Dù ngay từ đầu nó đã không nên là một hàm duy nhất
Sửa mười chỗ bị copy-paste dễ hơn rất nhiều so với gỡ một nút thắt bị buộc sai đã từng được buộc lại và đang níu giữ các mảnh của hệ thống
Nhìn bề ngoài có vẻ tự mâu thuẫn trong chủ đề này, nhưng nếu đọc chậm lại, có thể thấy họ đang dùng copy-paste như một tín hiệu cho biết code nào nên được trừu tượng hóa, và đâu mới là pattern thật sự cần tuân theo
Thật lạ khi người ta cứ lặp đi lặp lại đủ loại giới luật về phần mềm, gần như là các nguyên tắc mang tính tôn giáo
Trên giấy thì tất cả đều trông tuyệt vời và có vẻ như lẽ thường, nhưng sau 50 năm, phần mềm trong 90% trường hợp vẫn là rác
Vậy mà người ta vẫn tiếp tục lôi những thứ này ra như thể đó là insight thiên tài hay viên đạn bạc
Ở đây có một hệ quả rất hay. Code tệ khó loại bỏ hơn nhiều, nên nó tồn tại lâu hơn