1 điểm bởi GN⁺ 2023-11-16 | 1 bình luận | Chia sẻ qua WhatsApp
  • Cốt lõi là hai quy tắc tổ chức mã: kéo nhánh điều kiện về phía bên gọi và đẩy xử lý lặp sang phía phép toán theo lô
  • Khi đưa if lên trên, sẽ dễ ép buộc điều kiện tiên quyết bằng type hoặc assert hơn, đồng thời gom luồng điều khiển phức tạp vào một chỗ để dễ tìm các điều kiện trùng lặp và nhánh chết
  • Cấu trúc tạo enum rồi ngay sau đó lại match lại nó lặp lại cùng một điều kiện qua phân nhánh, cấu trúc dữ liệu và tái phân nhánh, nên có dư địa để đơn giản hóa thành các lời gọi foo(x)bar(y)
  • Khi đẩy for xuống dưới, phép toán theo lô trở thành đơn vị cơ bản thay vì lặp trên từng đối tượng, nhờ đó có thể khấu hao chi phí khởi động, thay đổi thứ tự xử lý, vector hóa và tối ưu kiểu struct-of-array
  • Khi đưa điều kiện ra ngoài vòng lặp, có thể giảm số nhánh trong hot loop, và điều này cũng dẫn tới các cấu trúc như TigerBeetle, nơi chi phí ra quyết định ở control plane được khấu hao bằng xử lý theo lô ở data plane

Đưa if lên trên

  • Các điều kiện if bên trong hàm trước hết nên được xem xét xem có thể chuyển sang phía bên gọi hay không
    • Ví dụ cho rằng một hàm nhận trực tiếp Walrus là hình thức tốt hơn so với một hàm nhận Option<Walrus> rồi trả về nếu là None
    • Thay vì kiểm tra điều kiện tiên quyết bên trong hàm rồi “không làm gì cả”, việc bên gọi kiểm tra và cưỡng chế nó bằng type hoặc assert sẽ rõ ràng hơn
  • Việc refactor đưa kiểm tra điều kiện tiên quyết lên trên có thể lan truyền dọc theo đường gọi, và kết quả là có thể giảm tổng số lần kiểm tra
  • Luồng điều khiển và if làm tăng độ phức tạp và dễ trở thành nguyên nhân gây bug
    • Khi đưa if lên trên, logic phân nhánh phức tạp sẽ được gom vào một hàm, còn công việc thực tế được đẩy xuống các routine con tuyến tính
    • Luồng điều khiển phức tạp sẽ dễ phát hiện điều kiện trùng lặp và điều kiện chết hơn khi nó nằm trong một hàm trên một màn hình, thay vì bị rải khắp cả file
  • Refactor “dissolving enum” là cách loại bỏ mẫu biến phân nhánh thành cấu trúc dữ liệu rồi lại phân nhánh lần nữa
    • Nếu f() tạo E::Foo(x) hoặc E::Bar(y) tùy theo điều kiện, rồi g(e) lại dùng match để gọi foo(x) hoặc bar(y), thì cùng một điều kiện đang bị lặp lại dưới nhiều hình thức
    • Nếu kéo điều kiện lên main(), cấu trúc có thể được đơn giản hóa thành if condition { foo(x) } else { bar(y) }

Đẩy for xuống dưới

  • Theo góc nhìn hướng dữ liệu, chương trình và hot path phần lớn xử lý nhiều đối tượng, nên ta đưa vào các đối tượng và lấy phép toán theo lô làm trường hợp mặc định
    • Phiên bản scalar cho từng đối tượng trở thành trường hợp đặc biệt của phép toán theo lô
    • for walrus in walruses { frobnicate(walrus) } là hình thức kém hơn frobnicate_batch(walruses)
  • Lợi ích chính là hiệu năng
    • Khi xử lý cả lô cùng lúc, có thể khấu hao chi phí khởi động
    • Có thể linh hoạt thay đổi thứ tự xử lý, đồng thời mở ra khả năng vector hóa hoặc cách tổ chức struct-of-array, nơi một trường của mọi thực thể được xử lý trước
    • Một trường hợp cực đoan được liên kết là Vectorized Interpreters Talk
  • Một ví dụ thú vị là phép nhân đa thức dựa trên FFT
    • Cách đánh giá đa thức đồng thời tại nhiều điểm có thể nhanh hơn so với việc đánh giá riêng lẻ từng điểm nhiều lần
  • Hai quy tắc iffor có thể được áp dụng cùng nhau
    • Hình thức tốt là phân nhánh theo if condition ở bên ngoài, rồi trong từng nhánh mới chạy for walrus in walruses
    • Hình thức xấu là đánh giá if condition ở mỗi lần lặp bên trong vòng for
    • Điều này giúp tránh đánh giá lại điều kiện, loại bỏ nhánh trong hot loop và tạo điều kiện cho vector hóa
  • Mẫu này áp dụng được cả ở mức vi mô lẫn vĩ mô
    • Kiến trúc của TigerBeetle là một cấu trúc khấu hao chi phí ra quyết định ở control plane bằng cách xử lý đồng thời các lô đối tượng ở data plane
    • Động cơ chính của lời khuyên về for là hiệu năng, nhưng cách vận hành theo đơn vị collection như jQuery đôi khi cũng giúp tăng tính biểu đạt

1 bình luận

 
GN⁺ 2023-11-16
Ý kiến trên Hacker News
  • Ban đầu tôi khá ngạc nhiên khi bài này bị phản đối như vậy, nhưng khi nhớ ra đây là lời khuyên về thiết kế hướng dữ liệu thì tôi hiểu hơn
    Giống như nhiều người trên diễn đàn này, phần lớn thời gian tôi cũng đang làm các ứng dụng web cho công việc, và trong bối cảnh đó lời khuyên này có thể trông như nói nhảm
    Nếu trong công việc hằng ngày bạn không phải bận tâm đến cache lệnh, thì heuristic rằng có thể gần như bỏ qua lời khuyên này có vẻ đúng
    Nếu muốn có cảm giác khi nào lời khuyên này trở nên quan trọng, nên tìm xem “Typical C++ Bullshit” của Mike Acton; bài này có cảm giác như đã sắp xếp lại nội dung đó theo cách dễ hiểu hơn
    Tôi khá đồng cảm với mối quan tâm của Casey Muratori, nhưng phần lớn phần mềm nghiệp vụ nên tối ưu cho khả năng thay đổi và tính đúng đắn hơn là hiệu năng, tức là “lập trình qua thời gian”

    • Tôi nghĩ lời khuyên này vẫn khá áp dụng được cho cả lập trình hướng đối tượng
      Các lập trình viên thường cố chia logic nghiệp vụ phức tạp thành những private method nhỏ trong class để làm cho DRY, còn “đưa if lên trên” giúp logic rẽ nhánh không bị rải rác qua nhiều method
      “Đưa for xuống dưới” cũng quan trọng. Nhiều luồng gọi cuối cùng kết thúc bằng các truy vấn cơ sở dữ liệu đắt đỏ, và thường xảy ra việc vòng lặp ở tầng trên tạo ra nhiều lần gọi DB ở tầng dưới
      Vòng lặp đó nhiều khi có thể được thay bằng mệnh đề where hoặc join trong SQL; các thao tác tổng hợp hay lọc cũng nên được đẩy gần DAO hơn thay vì lôi ra một đống object rồi lặp, vì như vậy dễ tối ưu gần DB hơn
      Tuy nhiên, như mọi nguyên tắc thiết kế, đừng áp dụng như giáo điều mà phải phán đoán có ý thức
    • Thiết kế hướng dữ liệu dường như luôn chọc vào cảm xúc của mọi người. Có lẽ vì nó hàm ý rằng nhiều phần trong cách tiếp cận hướng đối tượng đang thống trị hiện nay là sai
      Thật đáng tiếc khi xem khả năng thay đổi và hiệu năng là hai thứ đối lập. Trên thực tế tôi vẫn chưa thấy bằng chứng thuyết phục nào cho thấy cả hai xung đột với nhau
    • Khả năng thay đổi, tính đúng đắn và hiệu năng không loại trừ lẫn nhau; trái lại, chúng thường đi cùng nhau
      Một trong những khía cạnh quan trọng nhất của hiệu năng là làm cho mọi thứ nhỏ lại. Code nhỏ, cấu trúc dữ liệu nhỏ, số lệnh thực thi ít là cốt lõi, và “nghĩ đến cache lệnh” về bản chất cũng là viết code nhỏ
      Code càng nhỏ thì càng ít chỗ cho bug lọt vào, dễ hiểu trong một lần nhìn hơn, và cũng dễ đảm bảo độ phủ kiểm thử hơn, nên có lợi cho tính đúng đắn. Khả năng thay đổi cũng vậy: code càng nhỏ thì thay đổi càng nhỏ
      Tất nhiên cũng có những tối ưu làm code phức tạp hơn như song song hóa, caching, tối ưu mức thấp, nhưng đó chỉ là một phần của tối ưu hiệu năng; một lập trình viên nghiêm túc quan tâm đến hiệu năng sẽ không làm những việc đó nếu không profiling và phân tích
      if/forfor/if dù về chức năng không khác nhau nhiều, một bên vẫn có thể nhanh hơn; nếu có thể viết đúng mà không tốn thêm chi phí thì chẳng có lý do gì chọn phía chậm hơn
    • Nghe John Ousterhout nói thì trong thực tế, khả năng thay đổi·tính đúng đắn không xung đột với hiệu năng nhiều như ta tưởng lúc đầu
      Chương trình đơn giản có call stack ngắn, tránh các trừu tượng phức tạp và đắt đỏ, nên có xu hướng dùng ít bộ nhớ hơn và chạy nhanh hơn
      Nếu theo đuổi hiệu năng ở mức cao nhất, chương trình có thể trở nên phức tạp, nhưng sự đơn giản thật sự thường đem lại hiệu năng hợp lý
      “Đưa for xuống dưới” chủ yếu là lời khuyên hướng dữ liệu, còn “đưa if lên trên” gần với việc làm cho chương trình đơn giản hơn. Chính xác hơn, đó là nói về việc tăng tính cục bộ của mã nguồn, và điểm cốt lõi là gom logic rẽ nhánh vào một nơi: https://loup-vaillant.fr/articles/source-of-readability
    • Tôi nghĩ cả hai lời khuyên đều khá áp dụng được cho ứng dụng nghiệp vụ
      Đặc biệt “đưa vòng lặp xuống dưới” rất mạnh trong ứng dụng CRUD. Xử lý hàng loạt các thao tác tạo và cập nhật càng nhiều càng tốt thường tiết kiệm được lượng thời gian lớn hơn nhiều so với nút thắt CPU
      Sự khác biệt giữa items.map(insertToDb/postToServer)insertToDb/postToServer(items) gần như trong mọi trường hợp đều chênh nhau vài bậc độ lớn
      Tôi từng thấy các tối ưu kiểu này làm thời gian xử lý giảm từ mức giây/phút xuống mili giây, và trong nhiều trường hợp API cũng gọn hơn, log cũng dễ đọc hơn
  • Càng tích lũy kinh nghiệm, tôi càng cảm thấy có quá nhiều lập trình viên chú ý đến “code đẹp” ở đơn vị nhỏ, nhưng lại không đủ quan tâm đến thiết kế tổng thể của codebase.
    Các hàm ngắn gọn, được trau chuốt kỹ là điều tốt, nhưng những bài viết kiểu này có thể biến thành các cuộc tranh luận “nhà để xe đạp” vô bổ về năng suất trong PR hoặc thảo luận.
    Việc một hàm có hơi lộn xộn hay không, đặt iffor ở đâu, có dùng mapfilter hay không, tôi không quá bận tâm nếu tên hàm phù hợp, interface và type có tính biểu đạt, mục đích rõ ràng, có tài liệu, và không lạm dụng side effect.

    • “Đưa if lên trên” không phải là tranh luận nhà để xe đạp; ngược lại, nó gần với một lựa chọn kiến trúc cần quyết định như đã nói.
      Nếu nghĩ xem nên kiểm tra tính hợp lệ của input ở đâu, theo heuristic này thì nên kiểm tra ở tầng trên cùng nơi nhận input.
      Điều đó cũng giúp hiểu code, và về mặt chứng minh thì các tiền điều kiện cần được lan truyền lên trên, nên về kỹ thuật cũng là cần thiết.
      Lời khuyên đầu tiên chắc chắn không phải là tranh luận vụn vặt, còn lời khuyên thứ hai thì mơ hồ hơn một chút.
    • Khi làm về bảo mật, tôi đã tiếp xúc với nhiều tech lead và architect cỡ L7~L9 theo chuẩn FAANG, và những người ám ảnh nhất với “thiết kế đúng” thường là người có 5~10 năm kinh nghiệm.
      Họ cố giúp junior không tự bắn vào chân mình, nhưng thường vẫn chưa có đủ cảm giác về chi phí của độ phức tạp và code sẽ thay đổi thế nào trong dài hạn.
      Ngược lại, sau hơn 20 năm làm nghề, người ta thường đánh giá sự đơn giản cao hơn hầu hết các yếu tố kỹ thuật.
      Những câu hỏi như “nên rẽ nhánh sớm đến mức nào” hay “đoạn code này thực sự cần làm gì” thường đem lại câu trả lời có giá trị nhất về lâu dài.
      Các câu hỏi về abstraction và encapsulation lại dễ dẫn đến kiểu tranh luận đã nói ở trên; và việc các vấn đề bảo mật xuất hiện nhiều nhất trong code của những người chỉ nhìn “bức tranh lớn” thực ra cũng là vì họ không hiểu rõ codebase được thiết kế tốt đó đang làm gì.
    • Nếu những nguyên tắc trông có vẻ nhỏ nhặt này được định sẵn từ trước, các cuộc tranh luận tiêu hao cùng kiểu trong PR và thảo luận có thể giảm đi.
      Trong phần mềm nhạy cảm về hiệu năng, nơi thiết kế hướng dữ liệu phù hợp, cần quan tâm hơn đến những thứ ở đơn vị nhỏ như vậy. Vì tối ưu hóa của compiler hoạt động theo cách đó.
      Khi ấy luật chơi thay đổi. Ý nghĩa của từng câu lệnh trở nên quan trọng, code tự giải thích có ý nghĩa hơn, chú thích trong code chỉ còn để phục vụ suy luận, còn “tài liệu” trở nên gần với đặc tả và hướng dẫn người dùng.
    • Tôi cũng thấy nhiều trường hợp ngược lại. Có nhiều người thích thiết kế những thánh đường bị over-engineer, nhưng lại không nghĩ đến hiệu quả cấp thấp của thuật toán.
    • Một lý do là trong ngôn ngữ lập trình, chúng ta thiếu cách biểu đạt có ý nghĩa cho các component lớn hơn hàm rất nhiều, hoặc các component được kết nối theo cách khác với hàm.
      Cùng lắm là có thể gom các hàm lại với nhau, rồi từ đó trở đi thì tự xoay xở.
  • “Đưa if lên trên” có nhược điểm là tiền điều kiện và hậu điều kiện không hiện trực tiếp trong định nghĩa hàm, mà phải kiểm tra ở từng điểm gọi.
    Trong các dự án lớn có nhiều người tham gia, những hàm như vậy có thể bị tái sử dụng ngoài ngữ cảnh dự định và dẫn đến bug.
    Dùng contract framework có thể giải quyết, nhưng sẽ phải viết điều kiện hai lần trong contract và trong code; dependent type cũng tương tự.
    Cách gắn tag cho một vùng code thuộc một ngữ cảnh cụ thể, rồi định nghĩa các hàm chỉ có thể được gọi trong ngữ cảnh đó, là một hướng thú vị.
    Nếu là Python, có thể dùng decorator như @requires_context("VALIDATED_XY") cùng context manager validated_xy để chỉ cho phép gọi hàm trong vùng đã được validate.
    Runtime không biết ý nghĩa của ngữ cảnh đó, nhưng với tooling và test, ta có thể thiết kế sao cho chỉ thiết lập ngữ cảnh mong muốn khi một số điều kiện nhất định được thỏa mãn.
    Trong các ngôn ngữ như Haskell, có thể cưỡng chế ở cấp type bằng thứ như identity monad; và ngay cả khi không cưỡng chế ở cấp type, đó cũng có thể là một cách thú vị để bảo vệ các vùng code “unsafe”.

    • Có vẻ bạn đã bỏ lỡ phần thứ hai trong lập luận của tác giả. Ý là “đẩy việc kiểm tra tiền điều kiện sang caller, và có thể cưỡng chế bằng type”.
      Vì vậy tiền điều kiện vẫn hiện trực tiếp trong định nghĩa hàm, chỉ là không hiện dưới dạng câu lệnh if mà dưới dạng một phần của type signature.
      Đây là pattern phổ biến trong Rust được dùng trong bài, và khác với kiểm tra if, nó là tiền điều kiện nghiêm ngặt được kiểm tra ở compile time chứ không phải runtime; nếu không thỏa thì chương trình sẽ không compile.
    • Trong Python cũng có thể biểu diễn điều này bằng type.
      Ví dụ, nhận vào như do_something(position: ValidatedPosition), rồi sau khi validate một Position thông thường thì chuyển thành ValidatedPosition để truyền tiếp.
      Thực tế có thể sẽ encapsulate phần validate bên trong constructor của ValidatedPosition, nhưng điểm cốt lõi là nếu cố truyền thẳng Position thì mypy sẽ báo rằng bạn đã truyền sai type.
      Type checking của Python không toàn diện như Rust, nhưng nó ngày càng hữu ích khi muốn đảm bảo dữ liệu được truyền đi đã được xử lý phù hợp.
    • Lời khuyên “đưa if lên trên” không nói về kiểm tra tiền điều kiện, mà nói về lựa chọn đúng đường đi của code.
      Nếu một hàm có tiền điều kiện thì tất nhiên có thể assert ở đầu hàm. Ví dụ hệ thống type của Java cho phép null, nên một hàm cần object phải ném exception nếu nhận null.
      Mỗi điểm gọi có trách nhiệm chỉ gọi hàm khi tiền điều kiện là đúng. Theo định nghĩa của tiền điều kiện thì điều này là hiển nhiên.
      Gọi hàm khi vi phạm tiền điều kiện là bug của caller. Có thể cần code trong hàm để kiểm tra việc này nhằm tránh hành vi không xác định, nhưng cần phân biệt các assert như vậy với luồng điều khiển thực sự của chương trình; bài viết nói về vế sau.
    • Trong ví dụ fn frobnicate(walrus: Walrus) này, nếu truyền vào thứ không phải là Walrus mà bạn sở hữu thì chương trình sẽ không compile.
      Ngay cả khi là generic, đối số vẫn phải thỏa trait bound, và tùy vào cách dùng đối số trong hàm, compiler sẽ yêu cầu các bound cần thiết trong định nghĩa hàm.
    • Tôi nghĩ ý đồ của publicprivate phần nào là để gắn tag code theo một ngữ cảnh nhất định.
      Hoặc cũng có thể cần một ngữ nghĩa cụ thể hơn, cắt ngang các phạm vi mà public, privateprotected trong hệ sinh thái .NET đang xử lý.
  • Ví dụ đầu tiên không tệ vì iffor, mà vì lý do khác
    Nói chung, nếu có một “container” của thứ gì đó, thì tốt hơn là viết hàm cho “đối tượng” ở cấp miền bên trong nó, thay vì cho chính container
    Khi dùng agent trong Clojure, ta không viết hàm dành cho agent, mà viết hàm dành cho đối tượng mà agent có thể chứa
    Trong Elixir cũng vậy, các hàm miền cốt lõi thao tác trên cấu trúc dữ liệu miền bên trong chứ không phải PID, và khi cần thì lời gọi GenServer sẽ ủy quyền cho chúng
    Làm vậy linh hoạt hơn, và có thể tách bạch sạch sẽ hơn giữa miền cốt lõi “frobnicate Walrus” và mối quan tâm của ứng dụng “có thể có hoặc không có Walrus, nếu có thì frobnicate”

    • Lời khuyên được đưa ra có vẻ có khả năng đẩy logic kiểm chứng lên quá cao
      Kiểm chứng sớm là tốt, nhưng việc hàm đưa ra lỗi kiểm chứng rõ ràng thay vì đột ngột nổ với một lỗi kỳ lạ cũng rất quan trọng
      Haskell giải quyết vấn đề này bằng newtype, theo kiểu cung cấp một “container trong suốt chứng nhận rằng việc kiểm chứng phù hợp đã được thực hiện”
      Lời khuyên tôi thật sự muốn nhấn mạnh với mọi người là hãy ưu tiên “sad if”. Thay vì lồng nhiều lớp cho luồng bình thường, mã kiểm tra từng điều kiện sai ở phía trên rồi sửa ngay hoặc dừng lại hầu như luôn dễ đọc và dễ bảo trì hơn
      Con người có bản năng tập trung vào trường hợp kỳ vọng, nhưng mã thì ngược lại: tôi muốn nó tập trung trước vào các trường hợp ngoại lệ. Mỗi if đều tạo gánh nặng trong đầu, và nếu phải lấy thông tin từ hệ thống bên ngoài hoặc thoát sớm vì lỗi, thì khi phần phát hiện và xử lý nằm cạnh nhau, ta có thể trút bỏ gánh nặng đó ngay lập tức
    • Nếu ý là ví dụ đầu tiên sẽ tốt hơn nếu viết walrus.frobnicate(), thì tôi nghĩ đó gần với vấn đề sở thích cú pháp hơn là điểm cốt lõi mà tác giả muốn nói
  • Tôi không nói đây là lời khuyên tệ, nhưng cũng không nhất thiết là lời khuyên hay
    Việc ngôn ngữ được chọn là Rust nói lên khá nhiều điều. Hệ thống kiểu mạnh ngăn được nhiều kiểu lập trình phòng thủ vốn cần thiết ở các ngôn ngữ khác
    Nếu một lập trình viên C không kiểm tra tính hợp lệ của con trỏ được truyền vào hàm và gây ra tham chiếu ngược NULL, tôi sẽ không muốn làm cùng đội với người đó
    Vì vậy ít nhất một số if chắc chắn nên nằm ở phía dưới, và tốt hơn là lỗi được lan truyền lên trên một cách hợp lý
    Với for thì tôi không cảm thấy mạnh mẽ bằng, nhưng vì trong C, đối số mảng decay thành con trỏ, tôi nghĩ việc lặp cũng nên nằm ở trên chứ không phải dưới. Trong hàm nơi mảng được tạo ra thì có thể tin độ dài, nhưng trong hàm nhận nó làm đối số thì không

    • Giao diện trong C nên ghi tài liệu cẩn thận về các kỳ vọng, và chỉ kiểm tra đúng chừng đó
      Tài liệu nên thay thế cho hệ thống kiểu mạnh, chứ kiểm tra runtime thì không nên
      Mã đầy các kiểm tra NULL và những biện pháp phòng thủ khác khó đọc hơn nhiều
      Có thể nói ở ranh giới thư viện cần kiểm tra phòng thủ nhiều hơn, và bài viết này chính xác là đang nói hãy đẩy những kiểm tra đó lên trên
      Mã quan trọng về bảo mật có thể khác, nhưng trong phần lớn trường hợp, việc vô tình tham chiếu ngược NULL chỉ cần được bắt bằng kiểm thử, sanitizer và fuzzing là đủ
    • Trong C, NULL thường là một giá trị con trỏ hợp lệ nhưng không thể tham chiếu ngược
      Để kiểm tra một giá trị con trỏ không hợp lệ, bạn phải kiểm tra toàn bộ vô số giá trị sai có thể có; nếu chỉ kiểm tra NULL thì thực ra không phải đang kiểm tra giá trị không hợp lệ
      Nếu tiền điều kiện của hàm là “tham số p không được là NULL” thì có thể kiểm tra. Nhưng nếu tiền điều kiện là “p phải là một con trỏ hợp lệ”, thì chúc bạn may mắn tìm được điều kiện assert phù hợp
    • Heuristic của tôi là nếu hệ thống kiểu không ngăn được giá trị sai, thì trách nhiệm ngăn nó thuộc về runtime
      Dạo này tôi dùng T-SQL khá nhiều, mà không thể khai báo tham số hay biến là NOT NULL
      Vì vậy tốt nhất là kiểm tra NULL càng sớm càng hợp lý, thường là ở đầu stored procedure
      Nếu không, NULL có thể bất ngờ lan sâu xuống các tầng gọi và tạo ra vấn đề kém rõ ràng hơn
      May là dữ liệu bảng có thể khai báo NOT NULL, nên loại lỗi này thường không làm hỏng dữ liệu, nhưng bắt sớm thì dễ hơn nhiều
      Tuy nhiên nếu có logic ghi vào DB tùy theo một giá trị tham số nào đó, và giá trị đó bất ngờ là NULL, thì có thể ghi giá trị sai hoặc hoàn toàn không ghi giá trị cần thiết, về thực chất là làm hỏng dữ liệu
      Vì vậy lập trình phòng thủ là câu trả lời
  • Nếu nhìn không có ngữ cảnh phù hợp, lời khuyên này khá kỳ lạ, và có lẽ thậm chí là lời khuyên tệ
    Vòng lặp và câu lệnh if đều là phép toán luồng điều khiển, nên một số lập luận trong bài không hợp lý lắm
    Lập luận mạnh nhất có vẻ là hiệu năng, nhưng hiệu năng thường nên là vấn đề được quan tâm sau cùng, đặc biệt nếu đây chỉ là lời khuyên dạng heuristic
    Tiếc là tác giả đã biến nó thành một câu khẩu hiệu, và tôi hy vọng nó không lan rộng

    • Hiệu năng có thể là một lập luận, nhưng không phải lập luận mạnh
      Nếu có thể viết mã đã cải thiện như ví dụ của tác giả, thì điều kiện là hằng trong suốt thời gian chạy vòng lặp. Nếu không phải đánh giá một điều kiện đắt đỏ mỗi lần, dự đoán nhánh sẽ xử lý phần lớn
      Nếu điều kiện là biểu thức boolean từ các giá trị const, trình biên dịch cũng có khả năng nhận ra
  • Tôi đã nghĩ cả bài sẽ tiếp tục với ví dụ kiểu này: chia walrus theo điều kiện rồi gọi frobnicate_batch(fwalrus)transmogrify_batch(twalrus)
    Nhưng thực tế lại đi theo ví dụ dùng một điều kiện để bọc toàn bộ vòng lặp thành hai nhánh

  • Thật ngạc nhiên là các lập trình viên xuất thân từ nền tảng kỹ nghệ phần mềm lại thường làm sai chuyện này
    Tôi bắt đầu lập trình trong lĩnh vực khoa học, nơi việc nghĩ về những điều như thế này là tuyệt đối cần thiết
    Chỉ cần chọn sai thứ tự vòng lặp, mô phỏng có thể mất 1 tuần thay vì 1 giờ
    Nhờ nền tảng đó, tôi theo bản năng tối ưu nhỏ bằng cách sắp xếp thứ tự forif cho phù hợp, và mã không làm vậy trông đơn giản là sai

  • Tôi không chắc những quy tắc chung kiểu này có thật sự áp dụng được vào code thực tế hay không
    Những quy tắc như vậy thường trông như giáo điều đặt sai chỗ, và dù bài blog có mở đầu bằng cách nói rằng đó là heuristic, các lập trình viên trẻ không phải lúc nào cũng tiếp nhận theo cách đó
    Vài tuần trước YouTube cứ liên tục gợi ý cho tôi một video tên “I'm a never-nester”, có vẻ như lập luận rằng tuyệt đối không bao giờ được lồng if, và điều đó khá nực cười
    Như ví dụ trong bài, đoạn code có if condition bên trong for bị đánh dấu là “xấu”, nhưng trong phần lớn code thực tế được viết theo kiểu đó, condition phụ thuộc vào walrus nên không thể đưa if lên trên được
    Nếu có thể đưa lên, việc cùng một điều kiện bị đánh giá lại ở mỗi vòng lặp là quá hiển nhiên, nên lập trình viên thường tự nhiên sẽ tránh
    Nhưng khi một junior hoặc sinh viên đọc lời khuyên nghe như giáo điều, họ có thể cố tuân thủ nghiêm ngặt và tạo ra code tệ hơn

    • Tôi không đặc biệt giáo điều về chuyện “tuyệt đối không lồng”, nhưng với giả định cú pháp cho phép, theo kinh nghiệm tôi chưa từng thấy trường hợp nào mà cách dùng match (condition_a, condition_b) để chia bốn trường hợp lại không tốt hơn if lồng nhau
    • Tôi lại xem đây là một ví dụ tốt cho việc nên “đưa if lên trên”
      Mục đích của code là thực hiện một thao tác cụ thể lên walrus tùy theo điều kiện, nhưng thực ra if đang được dùng thay cho đa hình và hệ thống kiểu
      Tại sao walrus phải có hai hàm cần gọi trong các kịch bản khác nhau? Sao không có một hàm và hai kiểu, rồi truyền vào kiểu đúng?
      Ngay cả với cấu trúc hiện tại, ta vẫn có thể chọn hàm xử lý ở phía trên dựa trên điều kiện, rồi trong vòng lặp bên dưới gọi hàm đã chọn mỗi lần
      Nếu đưa ra quyết định sớm nhất có thể, ta không cần rải nó khắp code. Phần bên trong code thực hiện cùng một việc mỗi lần mà không phân nhánh, và đầu ra chỉ thay đổi thông qua đồ thị cấu hình
      Tất nhiên đây không phải ý tưởng mới; 15 năm trước nó đã là một ý tưởng cũ rồi: https://www.youtube.com/watch?v=4F72VULWFvc
    • Bản refactor “GOOD” chỉ hoạt động khi điều kiện không phụ thuộc vào walrus, và nó giúp làm rõ sự thật đó
      Nếu áp dụng lại “đưa for xuống dưới” ở đây, nó sẽ thành dạng gọi frobnicate_batch(walruses) hoặc transmogrify_batch(walruses) bên trong if condition
    • Tôi nghĩ việc những bài viết như vậy tồn tại là điều tốt
      Ai đó đã diễn đạt thành lời một vấn đề tôi từng gặp nhiều lần nhưng chưa diễn đạt tốt được, và nó có vẻ là một mô hình hay để giữ trong đầu
      Đồng thời, những phê bình kiểu này cũng có giá trị. Hy vọng các junior developer có xu hướng giáo điều sẽ đọc cùng và phán đoán tinh tế hơn một chút
    • Điều cốt lõi cần học từ mọi “heuristic” và “best practice” là lý do phía sau chúng
      Lập trình viên đặc biệt không nên sao chép rồi áp dụng máy móc những thứ này
      Áp dụng heuristic và best practice một cách mù quáng có thể không phải ý hay, và rốt cuộc mọi thứ đều “tùy tình huống”
  • Những heuristic như thế này thường chứa một chút trí tuệ, nhưng nhìn chung lại tạo ra thứ mà về sau phải gỡ bỏ tính giáo điều ở lập trình viên mới
    Luôn có nhiều trường hợp mọi thứ trở nên tệ hơn khi cố tuân thủ quá nghiêm ngặt, và “biết khi nào không nên nghe lời khuyên này” thực ra mới là độ khó cốt lõi

    • Dù sao thì việc gỡ bỏ tính giáo điều cũng là cần thiết
      Tôi nghĩ những quy tắc này hữu ích để đem ra thử nghiệm. Hãy chấp nhận chúng, đẩy đến tận cùng, rồi sau một ngày hoặc một năm thử đảo ngược lại, xem chúng đi được tới đâu
      Việc học các giới hạn cũng là một lớp vật liệu nữa được tích tụ trên tấm da thuộc palimpsest liên tục được viết đè
    • Bài viết này có thể hữu ích như một công án trong một tuyển tập lớn hơn
      Một số công án trong đó nên mâu thuẫn với nhau