Clean code #4: Viết các Hàm (Function) (P2)
Cùng tiếp tục sau Clean code #4: Viết các Hàm (Function) (P1), chúng ta đến với Phần 2
8. Convey Intent:
Để truyền đạt ý định (Convey Intent), lý do thứ ba để tạo ra một hàm.
🔴 Dirty
// Check for valid file extensions. Confirm admin or active
if(fileExtension == "mp4" ||
fileExtension == "mpg" ||
fileExtension == "avi") &&
(isAdmin || isActiveFile) {
// do Something
}
Với câu điều kiện này, bạn có thể thấy tác giả nhận ra rằng câu đang trở nên phức tạp và quyết định sử dụng comment để làm rõ hơn. Tuy nhiên, chúng ta có thể làm rõ hoàn toàn ý định của mình trong mã bằng cách tái cấu trúc Conditional này thành một phương thức có tên hay với các tên biến hay bên trong.
🟢 Clean
if(ValidFileRequest(fileExtension, isActiveFile, isAdmin)) {
// do Something
}
bool ValidFileRequest(String fileExtension, bool isActiveFile, bool isAdmin) {
bool validFileExtensions = new List<String>() {"mp4", "mpg", "avi"};
bool validFileType = validFileExtensions.contains(fileExtension);
bool userIsAllowedToViewFile = isActiveFile || isAdmin;
return validFileType && userIsAllowedToViewFile;
}
Một lần nữa chúng ta có thể tái cấu trúc vì method này thực sự thực hiện hai việc.
- Nó xác thực phần mở rộng tệp được yêu cầu và kiểm tra xem người dùng có quyền xem File hay không.
- Mỗi phần logic riêng biệt đó có thể được cấu trúc lại thành các phương thức riêng biệt để mỗi method có một trách nhiệm rõ ràng.
9. Do One Thing:
Do one thing
là lý do thứ tư và cuối cùng để tạo ra một function. Để duy trì tỷ lệ signal to noise ở mức cao, hàm của chúng ta phải thực hiện một việc. Có một số lợi thế khi đảm nhiệm một trách nhiệm duy nhất
- Nó hỗ trợ người đọc: Tên function/method được chọn phù hợp sẽ tóm tắt chức năng bên trong.
- Nó thúc đẩy việc tái sử dụng: Nó cũng thúc đẩy việc tái sử dụng vì các chức năng nhỏ tập trung dễ tái sử dụng hơn các chức năng lớn thực hiện nhiều việc.
- Nó giúp việc đặt tên và thử nghiệm dễ dàng hơn: Các function/method thực hiện một chức năng sẽ dễ đặt tên hơn nhiều và nếu một hàm khó đặt tên hoặc có tên quá rộng thì đó là dấu hiệu cho thấy có thể chia nhỏ hàm đó ra.
- Nó giúp chúng ta tránh các tác dụng phụ: Người đọc sẽ biết chức năng này thực hiện chức năng gì bằng cách đọc tên. Nếu có những điều bất ngờ khác có thể xảy ra bên trong, hãy cân nhắc việc tái cấu trúc thành một chức năng riêng biệt.
Tips: Không sử dụng cờ (flag) làm tham số hàm (function parameters)
Flag cho người dùng biết rằng function/method này thực hiện nhiều hơn một việc. Các hàm chỉ nên thực hiện một việc. Tách các hàm của bạn ra nếu chúng theo các đường dẫn mã khác nhau dựa trên boolean. Hãy xem ví dụ Dirty bên dưới:
🔴 Dirty
public void CreateFile(String name, bool temp) {
if (temp) {
fs.Create(`./temp/${name}`);
} else {
fs.Create(name);
}
}
Chúng ta hãy tái cấu trúc code này bằng ví dụ Clean:
🟢 Clean
public void CreateFile(String name) {
fs.Create(name);
}
public void CreateTempFile(String name) {
CreateFile(`./temp/${name}`);
}
10. Mayfly Variables:
Bạn có thể tưởng tượng việc đọc một chương trong một cuốn sách mà hàng chục nhân vật được liệt kê ở phía trước không? Sách truyền thống giới thiệu cho độc giả những nhân vật mới khi đến lúc họ thực sự tương tác với câu chuyện. Phần giới thiệu xuất hiện đúng lúc. Phần giới thiệu xuất hiện đúng lúc. Điều này tránh gây áp lực cho người đọc với nhiều thông tin ngay từ đầu mà chưa có giá trị gia tăng và hoàn toàn không có ngữ cảnh.
Khởi tạo tất cả các biến (varriable) của tôi cùng nhau ở trên cùng class. Vấn đề với cách tiếp cận này là nó hoàn toàn trái ngược với Quy tắc Bảy (Rule of Seven).
Rule of Seven: The reader has to keep track of all these variables throughout their reading and can't remove them from their finite memory until they go out of scope at theend of the function. Plus, anyone considering refactoring this code now has to consider the implications on the variables above.
Dịch nôm na là: "Người đọc phải theo dõi tất cả các biến này trong suốt quá trình đọc và không thể xóa chúng khỏi bộ nhớ hữu hạn của mình cho đến khi chúng nằm ngoài phạm vi ở cuối hàm. Thêm vào đó, bất kỳ ai cân nhắc việc tái cấu trúc mã này đều phải cân nhắc đến những tác động lên các biến ở trên."
Điều này tạo ra gánh nặng tinh thần không cần thiết vì bất kỳ biến số nào trong phạm vi cũng phải được xem xét. Các nhà phát triển chạy mã trong đầu khi họ đọc. Và việc có quá nhiều biến trong phạm vi giống như yêu cầu người đọc quay nhiều đĩa cùng một lúc. Thay vào đó, các hàm có cấu trúc tốt chỉ nên chứa Mayfly
Tóm lại: Nên cố gắng hạn chế tạo ra nhiều biến toàn cục, thay chúng thành các biến cục bộ của method nếu có thể.
What's a Mayfly Variable?
Chuồn chuồn (Mayfly) có tuổi thọ ngắn nhất trong số các loài sinh vật trên trái đất. Nhiều loài chỉ sống được 30 phút và loài già nhất chỉ sống được khoảng 24 giờ.
Chúng ta nên cố gắng mang lại cho các biến của mình tuổi thọ theo kiểu Mayfly. Có hai cách đơn giản để thực hiện điều này:
- Chúng ta nên khởi tạo biến (Variables) đúng lúc: Khi cần biến số, hãy đưa nó vào cuộc sống. Và khi không còn cần thiết nữa, hãy đưa nó ra khỏi phạm vi (out of scope) để gánh nặng tinh thần được trút bỏ.
- Nếu bạn đang tạo các hàm mục tiêu thực hiện một chức năng thì bạn sẽ tự động nhận được các biến Mayfly. Các function/method ngắn có nghĩa là các biến có thể xuất hiện và biến mất khỏi phạm vi một cách nhanh chóng, giúp người đọc bớt vất vả hơn.
11. How many parameters?:
Số lượng tham số (parameters) cao khiến hàm khó theo dõi và là dấu hiệu rõ ràng cho thấy hàm đó đang làm quá nhiều việc. Vì vậy, chúng ta nên cố gắng đạt được 0-2 tham số. Khi bạn tập trung vào việc viết các function/method nhỏ tập trung, việc đạt được mục tiêu này sẽ trở nên dễ dàng hơn và khá tự nhiên. Điều này sẽ giúp mã của bạn dễ đọc, dễ hiểu và dễ kiểm tra hơn.
🔴 Dirty
void SaveUser(User user, bool sendEmail, int emailFormat, bool printReport, bool sendBill) {
// do something
}
Các tham số cho biết câu chuyện khá rõ ràng. Hàm này được gọi là SaveUser
, nhưng rõ ràng nó xử lý nhiều hơn thế. Hàm này chấp nhận các tham số xác định xem có nên gửi email hay không và email đó phải ở định dạng nào. Nó cho phép người gọi chỉ định xem có nên in hay không. Và cuối cùng chỉ định xem người dùng có nên được gửi đến hóa đơn hay không. Email, in ấn và thanh toán đều là những vấn đề riêng biệt khi lưu người dùng mới và do đó, chúng cần được xử lý ở các chức năng riêng biệt. Vì vậy nên chuyển như bên dưới:
🟢 Clean
void SaveUser(User user) {
// do something
}
Một ví dụ khác:
🔴 Dirty
void SaveUser(User user, bool emailUser) {
// save user
if(emailUser) {
// email User
}
}
Các tham số Boolean này còn được gọi là Đối số cờ (Flag Arguments). Đối số cờ là một dấu hiệu rất mạnh cho thấy hàm đang thực hiện hai việc vì theo định nghĩa, khi một Boolean (emailUser) là true thì có một việc xảy ra, và khi một boolean là false thì sẽ có một việc hoàn toàn khác xảy ra. Ở đây bạn có thể thấy từ chữ ký (signature) hàm rằng nó vừa lưu vừa tùy chọn gửi email cho người dùng. Những function này có thể dễ dàng được phân chia để có thể tập trung vào một mục tiêu duy nhất. Như ví dụ bên dưới:
🟢 Clean
void SaveUser(User user) {
// save user
}
void EmailUser(User user) {
// email user
}
12. What's Too Long?:
Quá dài (Too long) là một phép đo rất tùy ý, nhưng tôi chắc rằng bạn có thể đồng ý rằng đến một thời điểm nào đó, một hàm sẽ quá dài và cần phải được chia nhỏ. Sau đây là một số dấu hiệu cho thấy hàm của bạn có thể đang quá dài.
a. Sử dụng quá nhiều dòng (line) trống và bình luận (comment):
Khi các hàm trở nên dài, các nhà phát triển thường dùng đến các dòng trống và chú thích để phân tách các dòng suy nghĩ, tương tự như cách tác giả sử dụng các đoạn văn. Tuy nhiên, những dòng trống và chú thích (comment) này thường là dấu hiệu cho thấy hàm này đang làm quá nhiều việc và có thể được hưởng lợi nếu tách thành các hàm riêng biệt có tên rõ ràng. Khoảng trắng và Bình luận chắc chắn đều là những công cụ hữu ích. Nhưng dù sao thì chúng cũng là dấu hiệu cho thấy một hàm đang trở nên quá dài. Nếu nó không vừa trên màn hình thì có lẽ đã đến lúc phải tách nó ra. Việc giữ cho các hàm ngắn như vậy cũng đảm bảo rằng người đọc không phải giữ quá nhiều biến số hoặc logic trong đầu cùng một lúc.
b. Một function sẽ dễ đặt tên nếu nó chỉ có một nhiệm vụ được xác định rõ ràng:
Vì vậy, nếu bạn gặp khó khăn trong việc nghĩ ra một cái tên mô tả đầy đủ chức năng của hàm thì có thể tên đó quá dài => Refactor lại thành các hàm riêng biệt.
c. Nhiều điều kiện tồn tại trong một hàm
Khi có nhiều Điều kiện tồn tại trong một hàm, hãy cân nhắc việc gọi một hàm riêng để trích xuất Điều kiện. Điều này cung cấp một mô tả rõ ràng về ý định dựa trên trạng thái của Câu điều kiện.
d. Các method khó hiểu thường hoạt động với nhiều hơn một lớp trừu tượng cùng một lúc
Một hàm phải hoạt động với một lớp trừu tượng (abstraction) duy nhất. Điều này giúp cho việc đọc và gỡ lỗi trở nên dễ dàng hơn. Ngoài ra, hãy nhớ lại Rule of Seven "Một hàm rất khó tiêu hóa với bảy tham số hoặc nhiều hơn bảy biến trong phạm vi tại một thời điểm nhất định". Vì vậy, hãy chú ý đến số lượng lớn của một trong hai tham số.
Trong cuốn sách Clean Code của mình, Bob Martin đưa ra một số hướng dẫn rõ ràng cho các hàm. Ông đề xuất rằng các hàm nên:
- Hiếm khi dài quá 20 dòng và hiếm khi dài quá 100 dòng.
- Các hàm không nên có nhiều hơn 3 tham số.
Viết các method nhỏ, có mục tiêu và đặt tên hay giúp tôi viết code dễ hơn và làm việc dễ hơn sau này. Trong hướng dẫn về style của Linux, họ nói rằng độ dài tối đa tỷ lệ nghịch với độ phức tạp và mức độ thụt lề của hàm đó. Vì vậy, nếu bạn có một function đơn giản về mặt khái niệm, chỉ là một câu lệnh trường hợp dài nhưng đơn giản thì bạn có thể viết một function dài hơn. Còn nếu bạn có một function phức tạp thì hãy tuân thủ chặt chẽ hơn các giới hạn.
Tóm lại:
- Các hàm đơn giản có thể dài hơn.
- Các hàm phức tạp phải ngắn gọn.
- Vâng, những điều này là tùy ý, nhưng hãy sử dụng phán đoán tốt nhất của bạn.
13. Exceptions:
Trong các function, mọi thứ có thể sai và xảy ra lỗi. Và Exceptions hữu ích để dừng hệ thống khi nó ở trạng thái nguy hiểm và bất ngờ. Ba loại ngoại lệ mà chúng ta sẽ thảo luận là: Unrecoverable
, Recoverable
và Ignorable
Unrecoverable | Recoverable | Ignorable |
---|---|---|
Null reference | Retry connection | Logging click |
File not found | Try different file | |
Access denied | Wait and try again |
a. Unrecoverable:
Unrecoverable exceptions là trường hợp phổ biến nhất. Ví dụ bao gồm các ngoại lệ tham chiếu (reference exceptions) null
và ngoại lệ không tìm thấy File khi File bị thiếu khiến ứng dụng không thể tiếp tục hoạt động ở trạng thái hữu ích (useful) và có thể dự đoán (predictable) được.
b. Recoverable:
Khi Recoverable exceptions xảy ra, không hẳn là mọi thứ đều mất, bạn nên thử lại lần nữa. Ví dụ về các exception này là cố gắng thiết lập lại kết nối cơ sở dữ liệu (database connection), thử một File khác hoặc chỉ cần đợi một lát để API của bên thứ ba hoạt động trở lại để bạn có thể thử lại. Với các Recoverable exception (ngoại lệ có thể phục hồi), điều quan trọng là phải cân nhắc đến việc từ bỏ việc thử lại tại một thời điểm nào đó để ứng dụng của bạn không bị kẹt trong vòng lặp vô hạn.
c. Ignorable:
Có một loại ngoại lệ hiếm hoi thực sự có thể Bỏ qua (Ignorable). Đã viết một hệ thống ghi nhật ký (logging) nhấp chuột để sử dụng trên các trang web. Nếu việc ghi nhật ký nhấp chuột không thành công, tôi đã chọn nuốt ngoại lệ và cho phép hệ thống tiếp tục bình thường vì nó không ảnh hưởng đến người dùng. Chúng tôi sẵn sàng chấp nhận không thu thập một số dữ liệu để đảm bảo người dùng không bị ảnh hưởng khi hệ thống ghi nhật ký nhấp chuột của chúng tôi bị lỗi.
Không có gì sai khi ghi nhật ký (logging) và cuối cùng là chấp nhận một ngoại lệ nếu bạn hoàn toàn hiểu rõ về những hàm ý. Nhưng hãy nhớ rằng những trường hợp này rất hiếm và điều quan trọng là phải suy nghĩ kỹ về những tác động tiếp theo đối với hệ thống này.
Lưu ý
- Bạn không bao giờ nên gặp phải trường hợp ngoại lệ (catch an exception) mà bạn không thể xử lý một cách thông minh, hãy để nó tự bộc phát.
- Nếu bạn gặp phải lỗi (error) mà bạn không thể chấp nhận, không thể giải quyết ngay tại chỗ và không thể giải quyết ở cấp cao hơn trong application thì application của bạn bị hỏng (broken). Hành vi đúng đắn khi một ứng dụng bị hỏng ( broken application) là sập (crash) ngay lập tức.
Một ứng dụng bị hỏng và cố gắng hoạt động trong trạng thái không nhất quán sẽ gây nguy hiểm cho chính ứng dụng, dữ liệu của ứng dụng và cuối cùng là người dùng. Cách tốt nhất để đảm bảo điều này xảy ra là đưa ra một ngoại lệ chưa được kiểm tra (throw an unchecked exception). Nếu ai đó biết cách xử lý lỗi (error) thì cuối cùng họ sẽ phát hiện ra. Nếu không, nó sẽ khiến ứng dụng của bạn, hoặc ít nhất là trường hợp sử dụng (use case) hiện tại, chết mà không gây ra thêm bất kỳ thiệt hại hoặc hỏng hóc nào.
Khi xem xét các trường hợp ngoại lệ, hãy nhớ rằng chỉ ghi lại (logging) lỗi thôi là không đủ. Trong ví dụ này, chúng ta đang đăng ký một speaker trong try
block và nếu nó không thành công, chúng ta sẽ ghi lại lỗi.
🔴 Dirty
try {
RegisterSpeaker();
} catch (Exception ex) {
LogError(ex);
}
EmailSpeaker();
Tuy nhiên, sau khi ghi lại lỗi, hệ thống chỉ tiếp tục gửi email cho speaker
. Đó là một vấn đề. Khối mã này không nên cho phép ứng dụng tiếp tục vì việc gửi email xác nhận cho diễn giả sau khi đăng ký không thành công có nghĩa là diễn giả sẽ được thông báo rằng họ đã đăng ký trong khi thực tế họ chưa đăng ký. Hãy suy nghĩ thật kỹ về những tác động của việc phát hiện ngoại lệ và cho phép hệ thống tiếp tục. Nếu hệ thống không thể tiếp tục một cách đáng tin cậy và hợp lý, hãy dừng xử lý.
🟢 Clean
RegisterSpeaker();
EmailSpeaker();
Để giữ cho các khối try/catch
dễ đọc, việc đặt phần thân của khối try
bên trong một hàm sẽ rất hữu ích.
🟢 Clean
void RegisterSpeaker() {
try {
// do something
} catch(Exception ex) {
LogError(ex);
}
}
Hãy tưởng tượng việc cố gắng đọc phiên bản này nếu phần bình luận quá dài. Có thể khó để biết khối try
bắt đầu và kết thúc ở đâu.
🔴 Dirty
try {
// Many
// lines
// of
// complicated
// And
// verbose
// logic
// here
} catch (ArgumentOutOfRangeException) {
// do Something here
}
Và quan trọng hơn, sử dụng một hàm như phiên bản Clean này cung cấp một tên rõ ràng cho phần code. Điều này giúp người đọc hiểu rõ chính xác những gì đang được thử trong khối try
.
🟢 Clean
try {
SaveThePlanet();
} catch (ArgumentOutOfRangeException) {
// do Somthing here
}
void SaveThePlanet() {
// Many
// lines
// of
// complicated
// And
// verbose
// logic
// here
}
14. Prioritize Private Method:
Trong một Class, nếu một chức năng chỉ được sử dụng bên trong nội bộ của Class, hãy cố gắng biến chúng thành Private Method. Bạn sẽ thấy được công dụng của điều này khi bạn xoá đi một tính năng không còn được sử dụng trong ứng dụng của bạn. Nếu chức năng đó nằm ở nhiều class, và bạn xoá các method tham chiếu (reference) đến chức năng đó, thì trong một class bất kì, các private method mà reference đến method bị xoá đó cũng sẽ được IDE thông báo đã không còn được sử dụng để bạn xoá đi. Điều này sẽ làm giảm thiểu các Zombie code trong project của bạn (Zombie Code sẽ được đề cập trong phần 7. Zombie Code của Clean code #6: Viết comment (P2)).
Điều gì xảy ra nếu bạn không ưu tiên việc cố gắng chuyển thành các Private method nếu có thể? Khi đó, IDE sẽ không thể phát hiện ra các method không còn được sử dụng nữa và không thông báo để bạn xoá code, vì chúng nghĩ các method này vẫn còn được sử dụng ở đâu đó. Hãy cùng xem ví dụ Dirty bên dưới:
🔴 Dirty
class User {
private String isNew;
void Login(){
if(!isNew) {
return;
}
if(!HasFacebookAccount()) {
return;
}
CheckPassword();
CheckUserName();
}
void HasFacebookAccount() { }
void CheckPassword() { }
void CheckUserName() { }
}
Như ví dụ trên, khi tính năng login với facebook không còn được sử dụng để chuyển sang login với email, phone hay một cái gì đó khác, thì bạn sẽ có nhu cầu xoá method LoginWithFacebook()
, nhưng sau khi bạn xoá method này bạn quên mất rằng cũng phải xoá method HasFacebookAccount()
hoặc thậm chí CheckPassword()
, CheckUserName()
và isNew
nếu nó không còn liên quan tới class Login
. Nhưng vì không có một dấu hiệu để bạn nhận ra phải xoá các method này, nên có thể chúng vẫn còn ở đấy để trở thành các Zombie code và bắt các lập trình viên bảo trì sau này phải chú ý đến chúng khi muốn tìm hiểu chức năng của class Login
. Vì vậy, bạn hãy cân nhắc biến đổi chúng như ví dụ Clean bên dưới:
🟢 Clean
class User {
private String isNew;
void Login(){
if(!isNew) {
return;
}
if(!HasFacebookAccount()) {
return;
}
CheckPassword();
CheckUserName();
}
private void HasFacebookAccount() { }
private void CheckPassword() { }
private void CheckUserName() { }
}
Lúc này nếu bạn xoá method Login
, thì IDE sẽ cảnh báo bạn các method như HasFacebookAccount()
, CheckPassword()
, CheckUserName()
và variable isNew
cũng nên được xoá vì chúng đã không còn được sử dụng ở bất kì đâu.
Cảm ơn bạn đã đọc hết bài viết. Nếu thấy hay hãy để lại mình 1 vote và hẹn gặp lại trong bài viết tiếp theo! 🚀
All rights reserved