2 điểm bởi GN⁺ 2025-03-12 | 1 bình luận | Chia sẻ qua WhatsApp
  • Ngay cả khi mã rất tốt về mặt logic, nó vẫn có thể khó đọc lâu dài; bài viết này truy tìm sự mệt mỏi đó ở độ phức tạp nhìn thấy bằng mắt
  • Halstead Complexity Metrics đếm số toán tử và toán hạng để tính VolumeDifficulty, và xem những cách triển khai có nhiều biến và toán tử hơn là khó hơn, dù có cùng hành vi
  • Cognitive Complexity của SonarSource đánh giá gánh nặng từ câu lệnh điều kiện, vòng lặp, xử lý ngoại lệ, tổ hợp toán tử logic, đệ quy và goto, xoay quanh cú pháp rút gọn, sự gián đoạn luồng tuyến tính và luồng điều khiển lồng nhau
  • Về biến, variable shadowing, tên tương tự nhau, khoảng sống của biến dài và các mẫu sử dụng lạ làm tăng chi phí theo dõi luồng dữ liệu của người đọc
  • Hàm nhỏ, mẫu quen thuộc, nhóm các chuỗi dài, điều kiện đơn giản, goto có giới hạn, lồng nhau nông, tên biến dễ phân biệt và khoảng sống biến ngắn trở thành tiêu chí về khả năng đọc có thể áp dụng vượt qua ranh giới ngôn ngữ và định dạng

Giới hạn và mục tiêu của các chỉ số khả năng đọc mã

  • Khả năng đọc mã không có một chỉ số đơn lẻ nào được dùng rộng rãi và đồng thuận
  • Các tài liệu tham khảo thường là bài báo học thuật không được dùng rộng rãi trong thực tế hoặc gần với ý kiến cá nhân, nên cần một công cụ thảo luận cụ thể hơn có thể dùng ngay trong code review
  • Mục tiêu không phải là phát minh chỉ số mới, mà là tập hợp các mẫu trực quan mà ai cũng có thể dùng khi nói về việc mã có dễ đọc hay không
  • Đối tượng xem xét là các phép đo hoặc ý tưởng thỏa mãn các điều kiện sau
    • Có thể áp dụng cho một đoạn mã nguồn hoặc một hàm đơn lẻ
    • Không chỉ tập trung vào độ phức tạp cốt lõi khó tách khỏi chính thuật toán triển khai, như Cyclomatic Complexity
    • Không chỉ dừng ở các yếu tố phong cách bề mặt như độ dài tên biến, khoảng trắng, thụt lề, cách đặt ngoặc

Halstead Complexity Metrics

  • Maurice Halstead đã đề xuất Halstead Complexity Metrics vào cuối thập niên 1970 để tạo ra các phép đo thực nghiệm cho mã nguồn
  • Chỉ số này có thể áp dụng qua nhiều ngôn ngữ và nền tảng, và tập trung vào hình thức mã được viết hơn là bản thân thuật toán đã triển khai
  • Các giá trị đo cốt lõi là bốn con số dựa trên toán tử và toán hạng
    • Số toán tử duy nhất n1
    • Số toán hạng duy nhất n2
    • Tổng số toán tử N1
    • Tổng số toán hạng N2
  • Dựa trên đó, Halstead tạo ra các chỉ số liên quan như length, volume, difficulty, và thậm chí cố gắng suy ra con số ước tính số lỗi có trong phần triển khai
  • Trực giác là càng nhiều toán tử thì càng phải xét nhiều tương tác tiềm ẩn hơn, còn càng nhiều toán hạng thì càng khó hiểu các khả năng của luồng dữ liệu
  • Ví dụ JavaScript

    • Với cùng một hàm xác định chẵn lẻ, cách triển khai đơn giản dùng ifreturn có ít toán tử và toán hạng hơn
    • 4 toán tử duy nhất, 7 toán tử tổng cộng
    • 5 toán hạng duy nhất, 6 toán hạng tổng cộng
    • Volume 33.30, Difficulty 2.50
    • Cách triển khai dùng mảng, Number, biểu thức so sánh và chỉ mục có nhiều toán tử và toán hạng hơn
    • 7 toán tử duy nhất, 10 toán tử tổng cộng
    • 9 toán hạng duy nhất, 12 toán hạng tổng cộng
    • Volume 71.35, Difficulty 3.75
    • Nhìn bằng mắt cũng thấy cách triển khai đầu tiên đơn giản hơn, và các giá trị Volume cùng Difficulty của Halstead cũng củng cố điều đó
    • Nhược điểm là không phải lúc nào cũng rõ trong mọi ngôn ngữ cái gì nên được xem là operatoroperand; khi đo, tốt hơn nên chọn một công cụ hoặc cách triển khai cụ thể và dùng nhất quán
  • Các mẫu thực dụng rút ra từ Halstead

    • Hàm càng nhỏ và càng ít biến thì thường càng dễ đọc
    • Toán tử đặc thù của ngôn ngữ hoặc cú pháp đường tắt tạo thêm gánh nặng cho người đọc, nên tốt hơn là không lạm dụng
    • Nếu nối chuỗi dài các thành phần hàm như map, reduce, filter, lambda, iterator, comprehension, thì dù ngắn gọn cũng có thể làm giảm khả năng đọc
    • Những chuỗi dài như vậy có thể xuất hiện thường xuyên hơn trong JavaScript và Rust, hoặc trong mã Python đi quá sâu vào itertools

Độ khó đọc theo Cognitive Complexity

  • Cognitive Complexity do SonarSource tạo ra là một chỉ số nhằm nắm bắt chính xác hơn độ khó khi đọc
  • Ý tưởng cốt lõi của chỉ số này gồm ba điểm
    • Cú pháp rút gọn kết hợp các câu lệnh làm giảm độ khó
    • Mỗi lần rời khỏi luồng tuyến tính thì độ khó tăng lên
    • Luồng điều khiển lồng nhau làm tăng độ khó
  • Có phê phán rằng tên gọi nghe như một chỉ số khoa học hoặc khách quan, nhưng về mặt thực dụng có thể xem đây là một heuristic hiệu quả
  • Vấn đề mật độ của cú pháp rút gọn

    • Cú pháp rút gọn như MyObj myObj = a?.myObj; ngắn hơn và tốn ít thời gian đọc hơn so với dạng if (a != null) { myObj = a.myObj; }
    • Tuy nhiên hai đoạn mã thực tế có thể không hoàn toàn giống nhau
    • Đoạn đầu khiến myObj trở thành a.myObj hoặc null
    • Đoạn thứ hai khiến myObj trở thành a.myObj hoặc undefined
    • Các ngôn ngữ có kiểm tra kiểu mạnh như TypeScript hay Rust cũng chỉ làm giảm khả năng bỏ sót, chứ không đảm bảo xử lý đúng mọi trường hợp
    • Nếu hỗ trợ kiểm tra kiểu yếu như JavaScript thông thường, khả năng các trường hợp góc cạnh như vậy không được xử lý sẽ cao hơn
    • Cú pháp rút gọn có thể dễ viết và dễ đọc, nhưng có sự đánh đổi giữa tính ngắn gọn và mật độ
  • Các yếu tố phá vỡ luồng tuyến tính

    • Mã tuyến tính không có điều kiện dễ lướt hơn mã có điều kiện
    • Cognitive Complexity xem không chỉ câu lệnh điều kiện, vòng lặp, goto, mà cả macro có điều kiện, try/except, chuỗi toán tử logic và đệ quy là các yếu tố làm tăng độ khó
    • switch được tính như một nhóm, nhưng chuỗi else-if được xem là khó hơn với mỗi else-if bổ sung
    • Lý do là else-if có thể thực hiện từ hai phép so sánh trở lên trong mỗi nhánh
    • Tuy vậy, fall-through trong switchbreak bị thiếu cũng có thể làm tăng độ khó đọc
    • Trường hợp nối tiếp cùng một toán tử logic trong biểu thức điều kiện và trường hợp trộn &&, ||, ! có độ khó khác nhau
    • debug || verbose || consoleMode là một biểu thức điều kiện đơn giản
    • debug || (verbose && consoleMode) khó đọc hơn do trộn toán tử
    • debug || !(verbose && consoleMode) còn phức tạp hơn vì có cả phủ định
  • Xử lý ngoại lệ và goto

    • try/catch làm tăng độ khó trong Cognitive Complexity, nhưng nhiều khối catch không được xem là khó hơn một khối catch; tryfinally bị bỏ qua
    • Bản thân hành vi ném ngoại lệ cũng có thể tạo ra chi phí đọc
    • Khi xử lý ngoại lệ vượt qua ranh giới hàm, độ phức tạp của các hàm liên quan bị đan vào nhau
    • Người đọc phải tìm ngoại lệ đó được bắt ở đâu
    • goto thường được tính là yếu tố làm tăng độ khó
    • Tuy nhiên, một số chuyên gia cho rằng dạng goto out hoặc goto done, dùng để giải phóng tài nguyên rồi thoát khỏi hàm trong điều kiện lỗi, có thể hữu ích
    • Ngược lại, goto vượt qua ranh giới vòng lặp theo cách không thể biểu diễn bằng continue hoặc break tạo gánh nặng đọc lớn, vì người đọc phải tái dựng một luồng điều khiển mới

Lồng nhau và hình dạng hàm

  • Nếu bản thân câu lệnh điều kiện đã khó đọc, thì câu lệnh điều kiện lồng nhau còn khó đọc hơn
  • Cognitive Complexity cộng thêm độ khó cho từng mức lồng nhau, bên cạnh điểm của chính câu lệnh điều kiện hoặc vòng lặp
  • Ý tưởng này còn được gọi bằng các tên như “Level of Indentation” hoặc “Bumpy Road”
  • Khi độ lồng nhau vượt quá 2 mức thì đặc biệt khó đọc; mã giảm lồng nhau bằng cách trả về sớm sẽ đọc phẳng hơn
  • Chỉ số này không phản ánh trực tiếp độ dài hàm, nhưng nếu các điều kiện khác giống nhau, hàm dài đòi hỏi nhiều công sức đọc hơn hàm ngắn

Tên biến, khoảng sống và mẫu quen thuộc

  • Tên dễ phân biệt và có tính mô tả

    • Tên có tính mô tả rất quan trọng để hiểu mã đang định làm gì, còn tên trùng lặp hoặc giống mật mã thì có tác dụng ngược lại
    • Variable shadowing là nguy hiểm
    • Nên tránh tình huống người đọc phải xét các quy tắc scope để phân biệt biến nào đang được dùng
    • Cũng nên tránh các định danh trông giống nhau về mặt thị giác
    • Những tên dễ nhầm bằng mắt như ij, itemitems có thể dẫn đến sai sót
    • Mã dùng nhiều biến thể của cùng một tên trong một hàm, như node, _node, thisNode, là dạng dễ tạo lỗi
  • Khoảng sống biến ngắn

    • Live variable analysis xem xét phạm vi từ điểm một biến được dùng lần đầu đến điểm cuối cùng nó có thể được dùng
    • Khi khoảng sống biến dài, người đọc phải giữ nhiều biến và nhiều giá trị có thể có hơn trong đầu
    • So với việc khai báo tất cả biến ở đầu hàm, khai báo ngay trước khi thực sự sử dụng có thể rút ngắn khoảng sống
    • Trường hợp tệ nhất là biến sống qua nhiều hàm và được dùng ở nhiều nơi
    • Nếu khoảng sống của nhiều biến về cơ bản giống nhau, object có thể phù hợp hơn
    • Nếu object không phù hợp, tốt hơn là giảm tối đa số hàm và số dòng người đọc phải đọc để hiểu giá trị
  • Chuỗi dài và biến trung gian

    • Phong cách lập trình hàm giúp khoảng sống biến ngắn, nhưng chuỗi quá dài hoặc callback lồng nhau có thể tạo gánh nặng đọc
    • Chia chuỗi hàm dài thành các nhóm nhỏ, dùng biến trung gian được đặt tên tốt hoặc hàm helper có thể giảm tải nhận thức cho người đọc
    • Phiên bản dùng biến trung gian có thể kém hiệu quả hơn một chút
    • Trừ khi công cụ hiệu năng cho biết dòng đó thực sự là nút thắt cổ chai, khác biệt hiệu quả vi mô như vậy không quan trọng
  • Tái sử dụng mẫu mã quen thuộc

    • Tái sử dụng mã và dạng biến quen thuộc giúp người đọc nhận ra các mẫu họ đã biết, nên có thể đọc đỡ vất vả hơn
    • Điều này liên quan đến Principle of Least Surprise
    • Trong một codebase, tốt hơn nên giữ nhất quán các dạng lặp lại, chẳng hạn cách viết câu điều kiện
    • Khi phải đi chệch khỏi mẫu, có thể làm nổi bật khác biệt bằng tên biến hoặc chú thích
    • Nếu đẩy suy nghĩ này đến cùng, hướng đi sẽ là dùng hàm template hoặc hàm generic để người đọc không cần nhận ra lại các mẫu lặp đi lặp lại

8 mẫu trực quan giúp tăng khả năng đọc

  • Line/Operator/Operand count: Hàm nhỏ hơn và ít biến/toán tử hơn thì dễ đọc hơn
  • Novelty: Tránh sự mới lạ trong hình dạng hàm, toán tử, cú pháp đường tắt; tái sử dụng các mẫu phổ biến của codebase
  • Grouping: Với chuỗi hàm dài, iterator, comprehension, hãy chia nhóm logic bằng hàm helper hoặc biến trung gian
  • Conditional simplicity: Giữ biểu thức điều kiện ngắn nhất có thể; trong một điều kiện, ưu tiên chuỗi cùng một toán tử thay vì trộn các toán tử logic khác nhau
  • Gotos: Không dùng goto trừ khi nó theo một mẫu xử lý lỗi cụ thể và các lựa chọn thay thế còn tệ hơn
  • Nesting: Giảm tối đa logic lồng nhau và các thay đổi thụt lề lớn; nếu cần lồng sâu, hãy tách thành hàm riêng thay vì chôn trong hàm lớn
  • Variable distinction: Dùng tên biến có tính mô tả và dễ phân biệt bằng mắt, đồng thời tránh variable shadowing
  • Variable liveness: Giữ khoảng sống biến ngắn, và đặc biệt chú ý các khoảng sống dài vượt qua ranh giới hàm

Các vấn đề lộ ra trong codebase thực tế

  • Codebase gây mệt mỏi tinh thần lớn có nhiều anti-pattern cùng lúc
  • Cụ thể là các hàm dài, sự pha trộn nhiều cấu trúc ngôn ngữ khác nhau, và nhiều chuỗi hàm lẽ ra nên được tách thành hàm helper
  • Kết quả là độ phức tạp lồng nhau và khoảng sống biến dài cùng tăng trong các hàm lớn
  • Dù chất lượng mã và tác giả đều cao, vẫn phát hiện một hoặc nhiều lỗi nghiêm trọng
  • Một trong số đó là lỗi dễ nhìn thấy, nhưng có khả năng đã bị bỏ sót vì nó nằm giữa một hàm dài và phức tạp nên khó suy luận
  • Người có khả năng đọc mã của bạn nhiều nhất sau một tháng chính là bạn, vì vậy viết theo dạng dễ đọc gắn trực tiếp với chi phí thực tế trong công việc

1 bình luận

 
GN⁺ 2025-03-12
Ý kiến trên Hacker News
  • Việc bài viết nói rằng chuỗi map/reduce/filter dài hoặc có nhiều chuỗi sẽ làm giảm khả năng đọc hoàn toàn không được nối tiếp bởi các nội dung khác
    Cảm giác như họ lén nhét vào một lời phàn nàn quen thuộc kiểu “vì không quen nên nó dở”, trong khi chỉ cần quen hơn một chút thì nhiều trường hợp nó dễ đọc và dễ viết hơn hẳn các phương án thay thế
    Ví dụ, tôi nghĩ khó mà đưa ra đoạn mã nào dễ đọc hơn books.filter(book => book.pageCount > 1000).map(book => book.author).distinct()
    Xét theo chỉ số độ phức tạp thì kiểu mã này cũng tốt hơn gần như mọi cách khác, và nên học ít nhất những nền tảng của lập trình hàm. Không cần phải giải thích đến monad, nhưng ít nhất cũng phải quen đủ để không tùy tiện chê bai mapfilter

    • Cách diễn đạt nghe sắc quá mức không cần thiết, nhưng ví dụ mà câu được trích dẫn nhắm tới không phải là những chuỗi ngắn như vậy
      Đại khái, tôi cho rằng từ khoảng chuỗi gồm 5 lời gọi nối tiếp trở đi thì bắt đầu khó đọc, và ví dụ trong bài cũng dài cỡ đó
      “multiple” nghĩa là có các chuỗi lồng nhau hoặc kiểu dữ liệu đang thao tác bị thay đổi; trong những trường hợp này tốc độ đọc sẽ chậm lại
      Các thành phần hàm có thể thanh nhã, nhưng cũng có thể bị dùng quá tay
    • Ví dụ chỉ là một bộ lọc đơn giản về mặt khái niệm trên một danh sách duy nhất
      Khi chuỗi quá dài, điều kiện trở nên phức tạp và có nhiều danh sách/biến, sẽ khó hiểu trong một lần đọc
      Trong vòng lặp thủ tục, có thể đặt tên cho kết quả trung gian, và nhờ cái tên đó ta có thể quên phần xử lý trước đó để tập trung vào bước tiếp theo
    • SELECT DISTINCT author FROM books WHERE pageCount > 1000;
    • Ví dụ thì ổn, nhưng tôi nghĩ lời phàn nàn ban đầu gần với trường hợp chuỗi rất dài hơn
      Cá nhân tôi thường tách chuỗi ra để đặt tên cho kết quả trung gian, cảm giác như dùng tên biến làm chú thích
      var longBooks = books.filter(book => book.pageCount > 1000)
      var authorsOfLongBooks = longBooks.map(book => book.author).distinct()
    • Các lời giải SQL mà mọi người đưa ra cũng hay, nhưng nếu là Prolog thì cũng có thể viết như thế này
      ?- setof(Author, Book^Pages^(book_author(Book, Author), book_pages(Book, Pages), Pages > 1000), Authors).
      Tùy cấu trúc cơ sở dữ liệu Prolog, còn có thể ngắn hơn nữa
      ?- setof(Author, Pages^(book(_, Author, Pages), Pages > 1000), Authors).
  • Tôi cho rằng mã tốt về cơ bản có một phần khá lớn mang tính định tính và văn chương
    Những lập trình viên hoặc người trong học thuật quen với tư duy toán học và muốn có câu trả lời định lượng thường thấy điểm này khó chịu
    Tôi thích cả Dostoyevsky lẫn Wodehouse; cả hai đều xuất sắc, nhưng cách viết rất khác nhau
    Dù viết mã không phải là một sân chơi mở đến mức đó, tôi từng làm việc với những codebase tốt nhưng có cảm giác khác hẳn nhau về chất, và cũng như việc làm quen với văn phong của một tác giả mới, thường phải mất thời gian mới “bắt được cảm giác” về phong cách của một codebase

    • Đồng ý 100%. Một trong những lời khen hay nhất tôi từng nhận được liên quan đến lập trình là “mã đọc như một câu chuyện
      Ý là tôi đã sắp xếp thứ tự hàm sao cho khi đọc tệp từ trên xuống dưới, mạch kể chuyện tự nhiên đi theo, và tạo ra một cách triển khai khai báo như thể đang trò chuyện với độc giả
      Tôi đi theo mô hình lập trình hàm thuần túy, và tôi nghĩ cách này hợp với phong cách giàu tính tự sự hơn
      Vì phụ thuộc/đầu vào của hàm chỉ giới hạn ở tham số hoặc các hàm thuần khác, còn đầu ra chỉ nằm trong kiểu trả về, nên dễ dẫn dắt độ phức tạp theo từng bước
      Khác với các mô hình khác có những phức tạp như trạng thái ẩn, tôi nghĩ trớ trêu là mô hình chính xác về mặt toán học nhất lại cũng phù hợp nhất với phong cách tự sự hơn
    • Nếu mất hơn 5 giây để đọc và hiểu mục tiêu cấp cao của một hàm, tôi coi đó là mã tệ
      Hình thức không quan trọng; nếu không thể biết hàm làm gì trong một khoảng thời gian hợp lý mà không cần nhiều năm kinh nghiệm phát triển, thì đó đơn giản là mã tệ
    • Nhiều mẫu cú pháp được xem là thanh nhã thực ra lại có cảm giác kém rõ ràng hơn cả toán học
      Ví dụ toán tử ba ngôi trong bài return n % 2 === 0 ? 'Even' : 'Odd'; có cảm giác như bộ não con người phải đọc ngược, và phù hợp với trình biên dịch xử lý cây cú pháp hơn là với con người
      Nếu là một nhà toán học, họ sẽ viết theo dạng hàm từng phần như n mod 2 = 0 thì là 'Even', nếu không thì là 'Odd'; cách đó rõ ràng hơn nhiều
    • Vì vậy code review rất quan trọng
      Nó giúp thành viên mới trong nhóm học được phong cách nhất quán, đồng thời giữ cho phong cách của nhóm nhất quán ở mức hợp lý
      Bình luận về .editorconfig cũng đáng tham khảo: https://news.ycombinator.com/item?id=43333011
      Nó giúp giảm việc tranh luận về các chi tiết phong cách trong pull request
    • Có lẽ đó cũng là lý do Literate Programming không được phổ biến rộng rãi
  • Bài viết hay, nhưng tôi nghĩ đã bỏ sót yếu tố gây mệt mỏi tinh thần nhất khi đọc code: tính khả biến
    Khi đọc một method, việc có thể “cố định” ý nghĩa của biến đúng một lần rồi giữ nguyên trong khi suy luận phần còn lại là một món quà lớn
    Việc hiểu một method nên tăng đơn điệu từ 0% lên 100%, chứ không nên phải khởi động lại method trong đầu vì bối rối không biết thân vòng lặp đã thay đổi accumulator như thế nào ở một lần lặp nào đó
    Lý do thực sự khiến GOTO có hại cũng nằm ở đây. Vấn đề không phải là khó di chuyển con trỏ lệnh trong đầu bên trong method, mà là khi có GOTO thì khó biết trạng thái của các biến khả biến

    • Không đồng ý. Có một không gian thông tin trừu tượng mà code đang mô hình hóa, và con trỏ lệnh trong đầu phải di chuyển trong không gian đó
      Cả biến khả biến lẫn bất biến đều có thể hỗ trợ hoặc cản trở việc di chuyển đó, tùy thuộc vào việc code tương ứng với không gian ấy gọn gàng đến đâu
      Biến bất biến có một chút lợi thế chiến thuật là không phải lo giá trị bị thay đổi hoặc bị đổi theo cách gây hiểu lầm, nhưng theo kinh nghiệm thì lợi thế đó không đủ lớn để biến thành quy tắc “luôn dùng tính bất biến”
      Đôi khi tính khả biến giúp biểu diễn không gian thông tin đó gọn gàng hơn nhiều
    • Tổng độ phức tạp không chỉ là vấn đề di chuyển con trỏ lệnh từ một điểm bắt đầu đã biết
      Nhìn từ phía được gọi chứ không phải điểm gọi, khi ai đó có thể nhảy tới một dòng cụ thể, ta không thể truy ngược trước đó đã xảy ra chuyện gì. Vì có thể đến từ bất cứ đâu, nên cần phân tích chương trình toàn cục thay vì phân tích cục bộ
      Nếu tính khả biến là nguồn gốc thật sự của độ phức tạp do GOTO, thì câu lệnh if và vòng lặp for cũng phải có cùng vấn đề
      Tôi đồng ý rằng tính khả biến và trạng thái trực tiếp tạo ra độ phức tạp, nhưng GOTO thuộc một phạm trù hoàn toàn khác và gây hại hơn nhiều
  • Một pattern mà cá nhân tôi không thích là trả về ngay trong if rồi để phần còn lại thành đường đi mặc định ngầm định
    if (n % 2 === 0) return "Even"; return "Odd"; đúng là ngắn hơn, nhưng tôi thích if ... return "Even"; else return "Odd"; hơn nhiều
    Lý do là cách đầu tạo cảm giác bất đối xứng. "Even""Odd" là hai lựa chọn đối xứng, nên cách có else trực quan hơn

    • Viết như return (n % 2 === 0) ? "Even" : "Odd"; là ít boilerplate nhất nên dễ đọc nhất
      Nếu ngôn ngữ có toán tử ba ngôi thì đáng lẽ phải nhận ra dễ dàng
    • Guard clause/trả về sớm có xu hướng chuyển trọng tâm của lập trình viên sang việc thu hẹp hành vi của hàm, thay vì đường đi bình thường ban đầu
      Theo kinh nghiệm, else tạo thêm lồng nhau và dễ khiến ta tiếp tục đánh giá các điều kiện biên hoặc biến vượt quá phạm vi tức thời của đường đi bình thường. Ta phải giữ toàn bộ ngữ cảnh đó trong đầu
    • Cá nhân tôi thích cách phía trước hơn
      Có thể thấy return ngay ở một mức thụt lề dưới tên hàm, và nếu không có kết thúc sớm thì tạo cảm giác rằng sẽ có một kết quả được bảo đảm
      Nếu return bị chôn sâu hơn bên dưới thì cảm thấy hơi kỳ
    • Tôi sẽ thêm dòng trống để return "Odd"; trông tách khỏi if, và nếu ngôn ngữ cho phép thì cũng thêm ngoặc nhọn cho thân if
      Cũng có tình huống cho phép else, nhưng thường là khi có side effect; thông thường refactor cho đến khi loại bỏ được nó sẽ rõ ràng hơn
      Code phức tạp thường được biến thành một chuỗi guard thoát ra theo thứ tự mức độ quan trọng hoặc chi phí thực thi, và giúp tách logic thực tế của function/method khỏi các điều kiện kết thúc
    • Tính bất đối xứng sẽ rõ ràng nếu refactor code sang kiểu continuation/callback hoặc sang cách biến đổi cấu trúc dữ liệu phức tạp hơn
      Cách thứ nhất chảy xuống dưới và thực thi tập lệnh thứ hai. return là toán tử đặc biệt cắt luồng điều khiển, nên luồng điều khiển thông thường của cách thứ nhất không thể hiện đúng tính đầy đủ của cả hai trường hợp
      Trong Rust theo idiom, trừ các trường hợp ngoại lệ phá vỡ luồng điều khiển của method, người ta không dùng return; ví dụ thứ hai thường xuất hiện nhiều hơn mà không có câu lệnh return
      Python cũng thường trả về sớm ở đầu khi tham số hoặc trạng thái sai, còn return ở vị trí cuối mới là giá trị trả về thực sự
      Vì các quy ước này, nếu phá vỡ cấu trúc if-else đầy đủ thì return được thụt lề sẽ trông như một tình huống ngoại lệ. Theo quy ước này, câu lệnh return tự nhiên trông như dư thừa trừ khi dùng để cắt luồng điều khiển, và quy ước của Rust trở nên dễ hiểu. Trong mọi ngôn ngữ, return là một câu lệnh ngang với break
  • Có lẽ chỉ mình tôi như vậy, nhưng TypeScript đôi khi khiến code khó đọc
    Nếu giữ data model ở mức khá “nguyên tử” và developer nghiêm túc trong việc thực sự khai báo và ghi tài liệu cho type thì ổn
    Nhưng khi type bắt đầu được suy ra từ type bằng utility type, và dựa vào suy luận type trong khi bỏ qua type tường minh, mọi thứ nhanh chóng bắt đầu bung ra
    Rất khó lần theo một field đến từ đâu trong một stack sâu như 4–5 tầng gián tiếp giữa các type. Một phần là suy luận, một phần là tường minh, một phần là type phái sinh và còn lẫn cả alias của field
    Với data model lớn và call stack sâu, dạng bỏ qua return type như function checkDogs(dogs: Dog[]) { ... } hoàn toàn không dùng được và thật sự khiến người ta phát điên

    • Tôi nghĩ function có lẽ nên khai báo output type
      Lý do chính là để buộc mọi đường trả về trong function đó tuân thủ type ấy
      Tôi đã thấy nhiều lần phát sinh regression khi thêm điều kiện mới nhưng trả về type hơi khác các nhánh khác
      Tuy nhiên, tôi không nghĩ việc gắn type vào khai báo biến có nhiều giá trị
      Trong ví dụ, const checkedDoggos = checkDogs([]) là ổn, cứ để checkedDoggos kế thừa type của function
      Tôi đang làm với một codebase mà linter bắt buộc const checkedDoggos: DogBreedAndSize[] = checkDogs([]), khá buồn cười và chẳng có mấy giá trị
    • Trong TypeScript, dù một phần return type được suy luận, tôi vẫn thích có chút thông tin type hơn là JavaScript hoàn toàn không có thông tin type
      Trong JavaScript, không thể chắc chắn nên phải liên tục đi lên đi xuống stack và giữ mọi thứ trong đầu
    • Quy tắc của tôi là chỉ gắn type khi trình biên dịch TypeScript kêu lên
  • Cần cẩn trọng với câu “hàm nhỏ có ít biến thường dễ đọc”
    Tôi không thích việc thảo luận về tính dễ đọc chỉ tập trung vào tính dễ đọc vi mô. Như vậy mã rất dễ bị chia nhỏ quá mức dựa trên giả định sai lầm rằng tính dễ đọc vi mô quan trọng hơn tính dễ đọc vĩ mô
    Sự giáo điều hóa này tạo ra những lập trình viên chỉ thấy cây mà không thấy rừng, và sinh ra mã cực kỳ kém hiệu quả hoặc khó gỡ lỗi
    Các ngôn ngữ thuộc họ APL nằm ở thái cực ngược lại, nhưng điểm tối ưu thực tế có lẽ nằm đâu đó ở giữa và khác nhau rất nhiều tùy từng người

    • Đặc biệt khi nhiều tệp đan xen với nhau, rõ ràng có một điểm trung gian
      Với mã chưa quen, chỉ cần nhảy tới định nghĩa 3–4 lần là đã thấy quá tải; có thể là vấn đề của tôi, nhưng khó mà tưởng tượng đa số người khác giỏi hơn tôi nhiều
      Trong văn hóa .NET, đặc biệt là “clean architecture”, vấn đề này nghiêm trọng đến mức gây sốc. Khi muốn sửa một tính năng hoặc truy vết một vấn đề, nó trải ra 4 tầng và 15 tệp, trong đó có tệp hơn 60% chỉ là từ khóa
      Tôi không biết nên kẻ ranh giới ở đâu, nhưng nhìn chung tôi thích một hàm dài có thể đọc tuần tự và tuân theo các khuyến nghị khác hơn là mã bị xé nhỏ đến mức cứ mỗi 5 dòng lại phải cuộn lên xuống
      Với kiểu/lớp cũng vậy: không cần phải đặt một enum 4 giá trị chỉ dùng trong DTO này ở một tệp riêng
  • Bài viết thú vị nhưng chưa làm tôi thỏa mãn
    Nó nhảy đến kết luận quá nhanh rồi lại quay về sở thích cá nhân. Tôi đồng ý với nhiều sở thích đó, nhưng bản thân bài viết đã nói rõ là muốn vượt ra khỏi chuyện sở thích
    Nhận định “toán tử hoặc cú pháp đường của từng ngôn ngữ là gánh nặng cho người đọc nên hãy tránh” không suy ra từ các chỉ số. Nếu một hàm có 3 toán tử khác nhau, và một toán tử đặc thù của ngôn ngữ thay thế được cả ba cùng lúc, thì “nỗ lực” của hàm sẽ giảm
    Các thành phần như map/reduce/filter nếu dùng tốt cũng có thể thay thế các toán tử khác và giảm “khối lượng”, nên có thể đi theo cả hai hướng
    Ví dụ ?. trông giống một chẩn đoán rất đặc thù theo ngôn ngữ, chỉ vào thiết kế khó đọc của JavaScript. Trong nhiều ngôn ngữ, nullundefined không tách biệt, nên nó thường được gọi là toán tử an toàn null
    “Che khuất biến (variable shadowing) là tệ hại” và “thời gian tồn tại dài khiến phải giữ nhiều biến hơn trong đầu” có thể mâu thuẫn với nhau
    Trong một số ngữ cảnh, tôi rất thích che khuất biến, vì nó loại bỏ phiên bản trước khỏi phạm vi thay vì để nó tiếp tục có thể truy cập

  • Có một plugin rất hay cho VS Code tên là Highlight
    Nó cho phép áp màu khác cho mã bằng biểu thức chính quy tùy chỉnh, và cách dùng phổ biến có lẽ là làm //TODO thành màu vàng
    Tôi dùng nó để làm mờ log, vì khi chèn log khắp nơi thì nhiễu thị giác rất lớn
    Thư viện tôi bảo trì dùng các log như this.logger?.info('Some logs here');, và tôi áp độ trong suốt 0.4 để chúng lùi xuống nền
    Chúng vẫn nhìn thấy được, nhưng nhìn lướt qua thì logic nghiệp vụ thực sự nổi bật hơn
    Có thể chỉnh cấu hình như sau: "highlight.regexes": { "((?:this\\.)?(?:_)?logger(?:\\?)?.(debug|error|info|warn)[^\\)]*\\)\\;)": { "regexFlags": "gmi", "decorations": [{ "opacity": "0.4" }] } }
    https://marketplace.visualstudio.com/items?itemName=fabiospa...

  • Tôi không đồng ý với nhận định rằng việc chia các chuỗi hàm dài hoặc callback thành các nhóm nhỏ và dùng biến có tên tốt là kém hiệu quả hơn một chút
    Hai phiên bản có thể hiệu quả y hệt nhau
    Trong cả hai trường hợp, các đối tượng giống nhau được cấp phát, lưu trên heap và trở thành đối tượng của garbage collection. Khác biệt về hiệu quả phụ thuộc vào trình biên dịch
    Ở phiên bản thứ hai, trình biên dịch nên nhìn ra rằng mỗi biến chỉ được dùng ngay sau khi khai báo, và có thể xử lý các đối tượng đó như nằm ngoài phạm vi giống như khi gọi theo chuỗi

    • Đồng ý. Sau khi biên dịch, rất có thể trình biên dịch không quan tâm việc giá trị trả về đã được đặt tên
      Tất nhiên là với giả định để trình biên dịch suy luận kiểu biến
      Chi phí thực tế thường thấy là khi bạn cụ thể hóa giá trị trung gian một cách tường minh để xem trong trình gỡ lỗi. Ví dụ, nếu biến nó thành danh sách thì sẽ phát sinh cấp phát lẽ ra có thể tránh được, nên có chi phí
  • Nỗ lực định lượng “tính dễ đọc” là điều tốt. Chúng ta cần nhiều cách tiếp cận như thế hơn nữa
    Định nghĩa phổ biến nhất hiện nay về tính dễ đọc có cảm giác gần như là “thứ tôi thấy dễ đọc”
    Có thể tìm ra các chiều thực tế của tính dễ đọc bằng cách cho rất nhiều người xem mã, yêu cầu họ chọn câu mô tả mã đó làm gì, rồi đo thời gian
    Những bài mà nhiều người nhất trả lời đúng trong thời gian trung bình ngắn nhất sẽ trở thành ví dụ về mã dễ đọc trong thế giới thực, và quan trọng hơn là có thể giúp nhận diện các thực hành thực sự khó đọc
    Người trả lời có lẽ sẽ được phân cụm theo các trục như “kinh nghiệm lập trình”, “có hiểu mô thức X không”, và khi xu hướng thay đổi, kết quả cũng có thể dịch chuyển theo thời gian

    • Một trong những khó khăn cốt lõi là chúng ta học cách đọc mã
      Việc ta đã học đọc và viết những gì sẽ định hình thứ ta cảm thấy dễ đọc
      Nhiều yếu tố có ảnh hưởng: bạn đang muốn làm gì, làm việc với ai, trước khi học lập trình bạn đã biết làm gì, bạn biết những ngôn ngữ nào khác, v.v.
      Sau khi hái hết những quả thấp, chẳng hạn vượt qua mức đừng đặt tên biến tùy tiện, không liên quan hoặc gây hiểu nhầm, rất nhiều vấn đề “tính dễ đọc” rốt cuộc có thể là chuyện xây dựng đồng thuận
      Có thể không có câu trả lời đúng vượt ra ngoài một nhóm lập trình viên cụ thể muốn làm việc cùng nhau
    • Tôi cho rằng điều đó không có mấy giá trị
      Tính dễ đọc của mã giống với tính dễ đọc của ngôn ngữ: phần lớn là vấn đề với người chưa rành ngôn ngữ đó, và có thể giải quyết bằng cách bỏ thời gian
      Vấn đề thực sự của lập trình là độ phức tạp của mã, và điều này không thể đánh giá chỉ bằng các chỉ số của từng mảnh mã riêng lẻ
      Vấn đề nằm ở quan hệ giữa các hàm, hơn là ở lựa chọn triển khai bên trong thân hàm
    • Cách tiếp cận đó quá một chiều
      Tìm ra mã làm gì thường là việc dễ; cái khó là sửa mã đó hoặc thêm tính năng
      Khó khăn này nảy sinh vì nhiều tầng trừu tượng che giấu cách chúng kết nối với nhau