Đôi khi cắm đầu comment là cách… ngu nhất để giải thích code
Khi tìm được một comment trong code hoàn toàn vô duyên và… vô dụng, bạn có thấy vui không? Đôi khi ham comment quá cũng không hay. Đây là một lỗi rất dễ gặp: Bạn thay đổi vài đoạn code, và quên xóa hoặc cập nhật comment. Một hai comment hiển nhiên không phá hư code, nhưng thử ...
Khi tìm được một comment trong code hoàn toàn vô duyên và… vô dụng, bạn có thấy vui không? Đôi khi ham comment quá cũng không hay.
Đây là một lỗi rất dễ gặp: Bạn thay đổi vài đoạn code, và quên xóa hoặc cập nhật comment. Một hai comment hiển nhiên không phá hư code, nhưng thử tưởng tượng lúc debug. Bạn đọc comment nói một thứ, trong khi code lại hoàn toàn đi theo hướng khác. Bạn chắc hẳn sẽ tốn rất nhiều thời gian mới biết được code để làm gì, hoặc tệ hơn, hoàn toàn đoán sai hướng!
Nhưng code mà không có comment là chuyện tuyệt đối không chấp nhận được. Trong hơn 15 năm kinh nghiệp lập trình, tôi chưa từng thấy codebase nào mà không cần đến comment.
Tuy nhiên, ta vẫn có cách giảm khối lượng comment xuống. Chúng ta có thể tận dụng nhiều kỹ thuật giải thích code khác, chỉ đơn giản với các tính năng của ngôn ngữ lập trình là đủ.
Kiểu code này thường gọi là self documenting. Trong bài viêt, tôi sẽ hướng dẫn các bạn sử dụng phương pháp này vào code. Đa số ví dụ được thể hiện bằng JavaScript, nhưng bạn hoàn toàn có thể áp dụng các kỹ thuật này vào các ngôn ngữ khác nữa.
Tổng quan về kỹ thuật
Một số lập trình viên thường gộp comments vào làm một phần trong self-documenting code. Nhưng trong bài viết, ta chỉ tập trung vào code, và sẽ bàn đến comment trong một bài viết riêng.
Chúng ta có thể chia kỹ thuật này thành ba loại chính:
- structural, làm rõ mục đích bằng cấu trúc của code hoặc thư mục
- naming related, như function hoặc tên biến
- syntax related, giải thích code bằng việc (tránh) sử dụng features của ngôn ngữ.
Về lý thuyết, nghe khá dễ. Và thách thức thực sự sẽ đến từ câu hỏi: khi nào ta dùng kỹ thuật này. Dưới đây là một số ví dụ trong thực tế.
Structural
Trước hết, hãy tập trung vào nhóm structural. Structural changes ám chỉ việc thay đổi code làm rõ hơn.
Chuyển code vào một function
Cách này cũng tương tự với việc tái cấu trúc “extract function” – nói cách khác, chúng ta di chuyển code đã có vào function mới: chúng ta “extract” code sang function mới.
Ví dụ, hãy đoán thử xem đoạn code sau làm gì:
1 2 3 |
var awidth = (value - 0.5) * 16; |
Chưa rõ lắm; có comment thì tốt rồi. Hoặc là, ta có thể extract function để biến thành self document:
1 2 3 4 5 6 7 |
var awidth = emToPixels(value); function emToPixels(ems) { return (ems - 0.5) * 16; } |
Tôi chỉ thực hiện một thay đổi duy nhất: chuyển calculation vào một function. Tên function sẽ miêu tả luôn mục tiêu, từ đó code không cần giải thích thêm nữa. Thêm vào đó, chúng ta giờ có thêm một helper function hữu ích để sử dụng, tránh lặp đi lặp lại.
Thay biểu thức điều kiện bằng function
Nếu mệnh đề có nhiều toán hạng, mệnh đề sẽ khó hiểu nếu không có comment. Chúng ta có thể áp dụng phương pháp tương tự như trên để làm rõ mệnh đề:
1 2 3 4 |
if(!el.offsetWidth || !el.offsetHeight) { } |
Mục đích của điều kiện trên?
1 2 3 4 5 6 7 8 |
function isVisible(el) { return el.offsetWidth && el.offsetHeight; } if(!isVisible(el)) { } |
Một lần nữa, chúng ta chuyển code vào một function và code ngay lập tức trở nên dễ hiểu hơn.
Thay biểu thức thành biến
Khi thế biến vào thành tố nào đó cũng tương tự như việc chuyển code vào function vậy, nhưng lần này ta sẽ không dùng function, mà chỉ đơn giản dùng một biến.
Hãy nhìn lại ví dụ mệnh đề if:
1 2 3 4 |
if(!el.offsetWidth || !el.offsetHeight) { } |
Thay vì extract một function, chúng ta cũng có thể làm rõ mệnh đề với biến:
1 2 3 4 5 |
var isVisible = el.offsetWidth && el.offsetHeight; if(!isVisible) { } |
Sử dụng biến có khi còn tốt hơn cả extract function – ví dụ như, khi logic (mà bạn đang muốn làm rõ) rất cụ thể với một thuật toán nhất định (chỉ được dùng ở một nơi).
Cách dùng thường thấy nhất của phương pháp này là ở biểu thức thuật toán:
1 2 3 |
return a * b + (c / d); |
Chúng ta có thể làm rõ biểu thức trên bằng cách tách phép toán ra:
1 2 3 4 5 |
var multiplier = a * b; var divisor = c / d; return multiplier + divisor; |
Nhìn chung, bạn có thể chuyển các biểu thức phức tạp sang biến để giải thích thêm cho các đoạn code khó hiểu.
Class và module interfaces
Interface – là các methods và properties công khai của một class hoặc module – có thể đóng vai trò documentation.
Hãy nhìn vào ví dụ sau:
1 2 3 4 5 6 7 8 9 10 11 |
class Box { setState(state) { this.state = state; } getState() { return this.state; } } |
Class này có thể chứa code khác nữa. Tôi cố ý để ví dụ thật đơn giản, để làm rõ chức năng documentation của public interface.
Cả hai functions đều có tên rất hợp lý: tự thể hiện vai trò của mình ngay trong tên. Nhưng dù vậy, ta vẫn chưa thấy được phải dùng chúng như thế nào. Có vẻ như, bạn sẽ phải đọc thêm code hoặc documentation cho class để biết được.
Vậy nếu chúng ta chuyển thành thế này:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
class Box { open() { this.state = 'open'; } close() { this.state = 'closed'; } isOpen() { return this.state === 'open'; } } |
Đã dễ thấy cách sử dụng hơn rồi đúng không? Hãy để ý, chúng ta chỉ mới thay đổi public interface; biểu diễn bên trong vẫn giữ nguyên như this.state property.
Giờ đây, khi nhìn qua bạn liền có thể biết ngay class Box được dùng như thế nào. Điều này chứng tỏ rằng mặc dù phiên bản đầu tiên đã đặt tên rất tốt trong functions, nhưng nhìn chung bẫn còn khá mơ hồ, và chỉ những decisions đơn giản như vậy, bạn có thể tạo hiệu quả vô cùng rõ rệt. Hãy luôn nghĩ đến bức tranh toàn cảnh.
Code grouping
Khi nhóm nhiều phần của code với nhau, bạn có thể tạo kết quả không kém gì documentation.
Ví dụ như, bạn nên luôn tìm cách khai biến càng gần với chỗ được sử dụng càng tốt, và cô gắng nhóm nhiều variable uses với nhau.
Có thể dùng cách này để ám chỉ mối quan hệ giữa nhiều phần của code, để bất cứ ai cần thay đổi code trong tương lai sẽ dễ dàng tìm chỗ phải sửa hơn.
Hãy xét ví dụ sau:
1 2 3 4 5 6 7 8 9 10 |
var foo = 1; blah() xyz(); bar(foo); baz(1337); quux(foo); |
Bạn có thấy foo được dùng bao nhiêu lần không? So với:
1 2 3 4 5 6 7 8 9 10 |
var foo = 1; bar(foo); quux(foo); blah() xyz(); baz(1337); |
Với tất cả cách dùng của foo nhóm với nhau, chúng ta có thể dễ dàng thấy được phần nào của code dựa vào đó.
Dùng pure functions
Pure functions dễ hiểu hơn nhiều nếu so với fuctions dựa vào state.
Pure fuction là gì? Khi call một function với cùng thông số, nếu function luôn xuất cùng một output, ta có thể gọi function này là “pure” function. Nói cách khác, pure fuction không được có tác dụng phụ hoặc phụ thuộc vào state – như thời gian, object properties, Ajax,…
Những kiểu function dễ hiểu hơn, vì bất cứ giá trị nào ảnh hưởng đến output sẽ dứt khoát bị bỏ qua. Bạn sẽ không phải chạy lanh quanh để hiểu được thành tố nào đó từ đâu chui ra, hoặc diều gì đang ảnh hưởng đến kết quả, vì tất cả đã có trước mặt rồi.
Một lý do khiến kiểu functions này phù hợp cho self-documenting code là vì: bạn có thể tin tưởng vào output của chúng. Dù gì đi nữa, function sẽ luôn trả output dựa trên thông số nào bạn nhập vào đó. Những functions này cũng sẽ không ảnh hưởng bất cứ thứ gì bên ngoài, từ đó sẽ không gây tác dụng phụ không lường trước được.
document.write() là một ví dụ cho tác dụng phụ điển hình. Nhiều người mới rất hay phạm lỗi này. Đôi khi function hoạt động trơn tru – nhưng những lần khác, trong tình huống nhất định, cả trang có thể bị xóa sạch trơn.
Cấu trúc directory và file
Khi nêu trên files hoặc directories, đặt tên đúng theo quy ước như trong project. Nếu không có quy ước rõ ràng trong project, đi theo chuẩn ngôn ngữ bạn lựa chọn.
Ví dụ như, nếu bạn thêm code mới liên quan đến UI, hãy tìm functionality tương tự trong project. Nếu code liên quan đến UI được đặt trong src/ui/, bạn hãy làm đúng như vậy.
Như vậy, dựa trên những gì đã biết về các phần code khác trong projcet, bạn có thể dễ tìm code và thể hiện mục đích của code hơn. Đến cùng, tất cả UI code đều ở một chỗ, nên chúng phải liên quan đến UI rồi.
Đặt tên
Có một câu nói về hai thứ khó nhất trong ngành khoa học máy tính:
Chỉ có hai thứ được xem là khó trong Computer Science: cache invalidation và đặt tên. Phil Karlton
Vậy thì, hãy xem thử ta nên đặt tên như thế nào để biến code thành self documenting.
Rename function
Function naming thường không khó, bạn hãy bám theo các quy tắc sau:
- Tránh dùng từ tối nghĩa như “handle” or “manage”: handleLinks(), manageObjects(). Cả hai functions trên làm cái gì?
- Dúng động từ chủ động: cutGrass(), sendFile() — cho các functions chủ động thực hiện hành động nào đó.
- Chỉ ra return value: getMagicBullet(), readFile(). Không phải lúc nào cũng khả thi, nhưng là được thì cứ làm.
- Các ngôn ngữ với phân loại mạnh mẽ cũng có thể sử dụng điểm đặc trưng của từng loại để ám chủ return values.
Rename variable
Với biến, có hai quy luật như sau:
- Chỉ rõ đơn vị: nếu bạn có thông số dạng số, bạn có thêm cả đơn vị dự tính vào đó. Ví dụ, awidthPx thay cho awidth để chỉ ra rằng giá trị có đơn vị pixel.
- Đừng dùng shortcut: tên a hay b không dùng được, trừ cho counters trong vòng lặp.
Tuân theo quy luật đặt tên đã có
Hãy cố gắng đi theo đúng một quy tắc đặt tên khi code. Ví dụ như, nếu bạn có một object của một kiểu cụ thể, hãy gọi cùng một tên:
1 2 3 |
var element = getElement(); |
Đừng tự nhiên gọi là node:
1 2 3 |
var node = getElement(); |
Nếu bạn theo cùng quy luật như những nơi khác trong codebase, người đọc nào cũng có thể đoán được ý nghĩa của các thành tố dựa trên những gì họ đã thấy trước đó.
Dùng errors “có ý nghĩa”
Undefined không phải một object!
Hãy tránh xa ví dụ của JavaScript, và làm sao để lỗi trong code luôn có message hữu dụng.
Error message hữu dụng là sao?
- Có miêu tả vấn đề là gì
- Nếu có thể, nên kèm theo giá trị biến hoặc dũ liệu gây ra lỗi
- Điểm mấu chốt: Lỗi sẽ giúp ta biết được trục trặc ở chỗ nào – từ đó đóng vai trò documentation chỉ ra function nên làm việc ra sao.
Syntax
Các method liên quan đến Syntax dùng cho self-documenting thường có xu hướng chuyên về ngôn ngữ. Ví dụ như, Ruby và Perl cho phép bạn làm đủ kiểu chiêu trò cú pháp, mà thông thường, cần nên tránh.
“Né” thủ thuật syntax
Đừng dùng những thủ thuật lạ lẫm. Sau đây là một cách bạn có thể làm mọi người nhầm lẫn:
1 2 3 |
imTricky && doMagic(); |
Tương đương với đoạn code “dễ chiều” hơn:
1 2 3 4 5 |
if(imTricky) { doMagic(); } |
Dùng hằng đã đặt tên, tránh magic values
Nếu bạn có special values trong code – như số hoặc string values chẳng hạn – hãy chuyển sang dùng hằng. Mặc dù trong có vẻ khá rõ bây giờ đấy, nhưng thường khi nhìn lại sau một hai tháng, sẽ chả có ai hiểu được con số đó lại được đặt ở chỗ này.
1 2 3 |
const MEANING_OF_LIFE = 42; |
(Nếu bạn không dùng ES6, bạn cso thể dùng var cũng phù hợp không kém.)
Tránh boolean flags
Boolean flags có thẻ làm code khó hiểu hơn. Hãy xem:
1 2 3 |
myThing.setData({ x: 1 }, true); |
true mang nghĩa gì? Bạn chắc hẳn không biết chút nào rồi, trừ khi đào sâu được vào nguồn setData().
Thay vào đó, bạn có thể thêm một function nữa, hoặc rename một function đã có:
1 2 3 |
myThing.mergeData({ x: 1 }); |
Giờ đây bạn đã thấy được điều gì đang diễn ra ở đây.
Tận dụng tính năng của ngôn ngữ
Chúng ta thậm chí còn có thể sử dụng một số tính năng trong ngôn ngữ để tuyền đạt mục đích code tốt hơn.
array iteration methods trong JavaScript là một ví dụ điển hình:
1 2 3 4 5 6 |
var ids = []; for(var i = 0; i < things.length; i++) { ids.push(things[i].id); } |
Đoạn code trên thu thập một loạt IDs thành array mới. Tuy nhiên, để biết được điều đó, chúng ta cần phải đọc cả phần thân của vòng lặp. So sánh nó với map():
1 2 3 4 5 |
var ids = things.map(function(thing) { return thing.id; }); |
Trong trường hợp này, chúng ta ngay lập tức biết được quy trình này sẽ tạo một array mới, vì đây là mục đích của map(). Cách này sẽ càng hữu ích nếu bạn có nhiều logic phức tạp.
Một ví dụ khác với JavaScript là keyword const.
Thông thường, bạn khai biến ở giá trị không bao giờ thay đổi. Một ví dụ thường gặp là khi load modules với CommonJS:
1 2 3 |
var async = require('async'); |
Bạn có thể làm rõ hơn về mục đích không thay đổi như sau:
1 2 3 |
const async = require('async'); |
Thêm vào đó, nếu có người vô tính thay đổi phần này, sẽ có xảy ra lỗi.
Anti-patterns
Những phương pháp trên vô cùng tiện dụng, nhưng vẫn còn vài đều bạn nên chú ý…
Extracting thay cho short functions
Nhiều người ủng hộ việc sử dụng các functions nhỏ xíu xiu, và nếu bạn extract mọi thứ, đó sẽ là thứ bạn nhận được. Tuy nhiên, việc này có thể gây nhiều bất lợi đến việc thông hiểu code.
Ví dụ, hãy tưởng tượng bạn đang debugging vài đoạn code. Bạn nhìn vào function a(). Sau đó, bạn có thể thấy nó dùng b(), rồi đến c(), và cứ tiếp tục như vậy.
Tuy short functions là một các rất hay và dễ hiểu, nhưng nếu bạn chỉ sử dụng function này ở một nơi duy nhất. Thay vào đó, ngãy dùng thử method “thay expression sang biến”.
Đừng gượng ép
Như mọi thứ khác trong lập trình, không có cách làm nào là tốt nhất cả. Bởi vậy, nếu cảm thấy cách nào đó không phải là ý kiến hay, đừng cố ép buộc.
Kết luận
Như vậy, với việc biến code thành seft document, ta sẽ tiếp kiệm được rất nhiều thời gian khi không cần phải viết comment khi không cần thiết, đặc biệt là khi cần bảo dưỡng và thay đổi code. Tuy vậy, bản thân self document vẫn không thể hoàn toàn thay thế documentation hay comment được.
Teachtalk via sitepoint