Các mẫu độ phức tạp trực quan làm giảm khả năng đọc của mã (2023)
(seeinglogic.com)- 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 Volume và Difficulty, 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,
gotocó 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
- Số toán tử duy nhất
- 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
ifvàreturncó í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
Volume33.30,Difficulty2.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
Volume71.35,Difficulty3.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ị
VolumecùngDifficultycủ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à operator và operand; 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
- Với cùng một hàm xác định chẵn lẻ, cách triển khai đơn giản dùng
-
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ạngif (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
myObjtrở thànha.myObjhoặcnull - Đoạn thứ hai khiến
myObjtrở thànha.myObjhoặcundefined - 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ú pháp rút gọn như
-
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ỗielse-ifđược xem là khó hơn với mỗielse-ifbổ sung- Lý do là
else-ifcó 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
switchvàbreakbị 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 || consoleModelà một biểu thức điều kiện đơn giảndebug || (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/catchlàm tăng độ khó trong Cognitive Complexity, nhưng nhiều khốicatchkhông được xem là khó hơn một khốicatch;tryvàfinallybị 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
gotothườ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 outhoặcgoto 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,
gotovượt qua ranh giới vòng lặp theo cách không thể biểu diễn bằngcontinuehoặcbreaktạ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ư
ivàj,itemvàitemscó 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
gototrừ 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
Ý 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
mapvàfilterĐạ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
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;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()?- 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
Ý 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
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ệ
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ườiNế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 = 0thì là'Even', nếu không thì là'Odd'; cách đó rõ ràng hơn nhiềuNó 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ề
.editorconfigcũng đáng tham khảo: https://news.ycombinator.com/item?id=43333011Nó giúp giảm việc tranh luận về các chi tiết phong cách trong pull request
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
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
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
ifvà vòng lặpforcũ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
ifrồi để phần còn lại thành đường đi mặc định ngầm địnhif (n % 2 === 0) return "Even"; return "Odd";đúng là ngắn hơn, nhưng tôi thíchif ... return "Even"; else return "Odd";hơn nhiềuLý do là cách đầu tạo cảm giác bất đối xứng.
"Even"và"Odd"là hai lựa chọn đối xứng, nên cách cóelsetrực quan hơnreturn (n % 2 === 0) ? "Even" : "Odd";là ít boilerplate nhất nên dễ đọc nhấtNếu ngôn ngữ có toán tử ba ngôi thì đáng lẽ phải nhận ra dễ dàng
Theo kinh nghiệm,
elsetạ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 đầuCó 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ỳ
return "Odd";trông tách khỏiif, và nếu ngôn ngữ cho phép thì cũng thêm ngoặc nhọn cho thânifCũ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ơnCode 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
Cách thứ nhất chảy xuống dưới và thực thi tập lệnh thứ hai.
returnlà 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ợpTrong 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 returnPython 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ệnhreturntự 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ữ,returnlà một câu lệnh ngang vớibreakCó 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ênLý 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ứ đểcheckedDoggoskế thừa type của functionTô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 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
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
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/filternế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ướngVí 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ữ,nullvàundefinedkhô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
//TODOthành màu vàngTô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ềnChú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
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
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í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
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