4 điểm bởi GN⁺ 2024-10-30 | 1 bình luận | Chia sẻ qua WhatsApp
  • 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 để util dướ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 util duy nhất sẽ cứ phình ra mãi, và khi đã quá to thì rất khó tách nhỏ
  • 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 requests là ví dụ về việc cung cấp giao diện đơn giản bên trên urllib3 vốn dài dòng hơn
    • requests xử lý workflow phổ biến khi dùng HTTP và che đi nhiều chi tiết thực dụng
    • urllib3 cung 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

 
GN⁺ 2024-10-30
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_...

    • Đúng, nhưng khi áp dụng nguyên tắc đơn giản là vững chắc thì cũng phải hiểu độ phức tạp nội tại
      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
    • Quy tắc tôi theo là thế này: lần đầu thì cứ viết, lần thứ hai thì sao chép, lần thứ ba thì cân nhắc refactor
    • Tôi đồng ý, nhưng không chắc cách diễn đạt simple is robust có đủ trực quan không
      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
    • Đồng ý cực kỳ mạnh mẽ. Quá nhiều rác rưởi trong phần mềm sinh ra từ việc cố giải quyết những vấn đề tưởng tượng
      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?
    • Độ phức tạp của business logic không biến mất chỉ vì ta mong muốn. Nếu nó đồ sộ và đan xen thì code cũng sẽ như vậy
  • 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

    • Bạn có thể làm cho nó dễ gỡ bỏ, nhưng người khác vẫn có thể sẵn sàng tạo abstraction và logic rồi nhét vào lõi dự án, để sau này nó đông cứng đến mức gần như không thể loại bỏ
      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 đó
    • Dù vậy vẫn tùy tình huống. Nếu là ứng dụng nghiệp vụ thì đúng, đúng gấp mười lần
      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
    • Có thứ sẽ thay đổi, và đôi khi ta chọn abstraction sai
      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

    • Nói đơn giản hơn một chút, nếu tạo ra thứ dễ xóa, thì khi xóa bạn sẽ không vô tình tạo bug
      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 số dòng code nhiều, có thể dự đoán rằng số dòng test cũng sẽ phát sinh ở mức nào đó tương ứng
      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
    • Test là tốt, nhưng lập trình không kết thúc chỉ bằng việc viết test. Không cần bài viết nào cũng phải nhắc đến 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

    • Theo kinh nghiệm của tôi, code copy-paste tệ thường kết thúc bằng một buổi chiều khó chịu để trả nợ kỹ thuật và sửa lỗ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
    • Code được tái sử dụng thường là code đúng ở nhiều nơi, nên muốn thay đổi thì phải giảm tốc độ lại và tách rời các điểm đó
      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 trong 10 lời gọi hàm, 3 cái cần đổi theo một cách, 5 cái theo cách khác, còn 2 cái còn lại không còn dùng cùng một trừu tượng nữa nên phải viết lại hoàn toàn, thì sẽ thế nào
      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
    • Nếu là tác giả bài viết, có lẽ họ sẽ trả lời rằng đoạn code đó nên được chuyển vào một module hoặc một hàm
      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

    • Tôi cho rằng 90% rác đó là do những người không đọc hoặc không viết các bài như thế này tạo ra
  • Ở đâ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