Cách đây khoảng mấy chục năm, có lẽ điều quan trọng nhất khi bạn code chỉ là sự hiệu quả và tối ưu. Khi đó RAM của máy tính còn được tính bằng byte thay vì giggabyte, tài nguyên hạn hẹp đến mức mỗi lúc máy tính chỉ có thể chạy một chương trình. Những ngày đó đã xa rồi.
Cùng với tốc độ phát triển nhanh chóng của phần cứng, tính hiệu quả tụt lại phía sau và nhường chỗ cho một yếu tố quan trọng hơn, viết code đẹp, dễ đọc và dễ hiểu. Điều này xuất phát từ thực tiễn là quy mô của các project trở nên lớn hơn, các mô hình Agile đòi hỏi sự cộng tác giữa mọi người nhiều hơn, môi trường và requirement thay đổi nhanh chóng đòi hỏi code phải dễ dàng thay đổi và bảo trì.
Tuy nhiên, khi mới bắt đầu chúng ta thường có thiên hướng viết code xấu. Có thể vì lười, hoặc có thể bạn nghĩ không có đủ thời gian để làm công việc tốt hơn, rằng boss sẽ tức giận vì bạn dành quá nhiều thời gian để viết code đẹp hơn. Cũng có thể bạn quá mệt mỏi khi viết một chương trình nào đó và chỉ muốn làm cho xong thôi. Và thế là những đống code thập cẩm ra đời.
Những lúc nhìn lại đống code đó, chúng ta lại muốn để lại đến ngày hôm sau. Chúng ta thở phào nhẹ nhõm khi thấy chương trình vẫn hoạt động bình thường, và cho rằng một đống code lộn xộn mà chạy được vẫn tốt hơn là không có gì. Tất cả đều ổn cho đến ngày bug xuất hiện, hoặc khách hàng ngẫu hứng muốn thêm một vài thay đổi để ứng dụng ngầu hơn. Và mọi rắc rối bắt đầu.
Viết code đẹp đòi hỏi chúng ta phải hình thành được cảm giác về code, cảm giác về tính sạch đẹp của nó (code-sense). Có những người sinh ra đã có cảm giác này rồi, những người khác phải luyện tập và học hỏi mới đạt được. Nó không chỉ giúp chúng ta nhìn ra một đoạn code là tốt hay xấu, mà còn cho chúng ta thấy những phương pháp, cách thức để biến code xấu thành code đẹp.
Tôi muốn code của mình phải tinh tế và hiệu quả. Logic phải rõ ràng để bug không thể ẩn nấp được, sự phụ thuộc lẫn nhau giữa các thành phần được tối thiểu hoá để bảo trì dễ hơn, hiệu năng gần như tối ưu để không khiến người khác làm code lộn xộn bằng những đoạn tối ưu không có quy tắc. Code đẹp là code làm một việc tốt. Bjarne Stroustrup, inventor of C++ and author of The C++ Programming LanguageCode đẹp là code đơn giản và ý đồ rõ ràng. Code đẹp đọc như một đoạn văn hay. Code đẹp không làm mơ hồ ý đồ của người viết, ngược lại là sự kết hợp của sự trừu tượng hoá và những câu lệnh điều khiển rõ ràng.Grady Booch, author of Object Oriented Analysis and Design with ApplicationsCode đẹp là code có thể đọc và cải thiện bởi những người khác. Có unit test và acceptance test. Code đẹp dùng cách đặt tên có nghĩa, cung cấp một cách xử lý duy nhất cho mỗi mục đích, sự phụ thuộc lẫn nhau được tối thiểu hoá, cung cấp API rõ ràng và đơn giản.
“Big” Dave Thomas, founder of OTI, godfather of the Eclipse strategy
Giới thiệu dài dòng đủ rồi, sau đây chúng ta sẽ đi vào phần chính. Các quy tắc sẽ được trình bày một cách ngắn gọn nhất có thể, kèm với ví dụ.
Chọn tên đúng có thể mất thời gian suy nghĩ, nhưng đảm bảo sẽ tiết kiệm cho bạn nhiều thời gian hơn về lâu dài. Vì thế nên chọn tên cẩn thận và đổi tên khi bạn tìm được từ tốt hơn.
Tên của biến, hàm hoặc class phải nói lên vì sao nó tồn tại, nó làm gì và được sử dụng như thế nào. Nếu tên biến cần phải chú thích mới hiểu được, đó là tên chưa thể hiện được ý đồ.
Tên biến chưa tốt:
int d; // elapsed time in days
Tên biến tốt:
int elapsedTimeInDays;
int daysSinceCreation;
int daysSinceModification;
int fileAgeInDays;
Tìm kiếm dễ dàng và nhớ nhanh hơn.
Sử dụng:
class Customer {
private Date generationTimestamp;
private Date modificationTimestamp;;
private final String recordId = "102";
/* ... */
};
Thay vì:
class DtaRcrd102 {
private Date genymdhms;
private Date modymdhms;
private final String pszqint = "102";
/* ... */
};
Mã hoá tên biến chỉ làm chúng ta mất công giải mã. Một ví dụ về mã hoá tên phổ biến trước đây là Hungarian Notation
, được thực hiện bằng cách thêm một vài chữ cái biểu thị kiểu ngay trước tên biến, ví dụ txtName, iAge...
Điều này đặc biệt đúng với Java, là một ngôn ngữ có quy định chặt chẽ về kiểu. Các công cụ phát triển (IDE) cũng đã đủ mạnh để highlight các biến quan trọng và có thể phát hiện các lỗi về kiểu ngay từ khi chưa biên dịch code. Vì thế mã hoá chỉ làm việc đổi tên biến, hàm, class trở nên khó hơn. Đồng thời việc đọc code cũng vướng víu hơn.
PhoneNumber phoneString;
// name not changed when type changed!
Hạn chế việc người đọc code phải dịch tên bạn đặt ra sang một tên khác mà họ biết. Vấn đề này có thể xảy ra khi bạn sử dụng những tên không nằm trong domain của bài toán đặt ra, hoặc sử dụng tên khác với tư duy thông thường.
Ví dụ rõ nhất là đặt tên biến chỉ có một chữ cái và sử dụng các hằng số magic. Ví dụ:
int i, j;
int secondsInADay = 24 * 60 * 60;
Viết lại đoạn trên một cách rõ ràng hơn:
static final int SECONDS_IN_A_MINUTE = 60;
static final int MINUTES_IN_AN_HOUR = 60;
static final int HOURS_IN_A_DAY = 24;
int numberOfEmployees, numberOfRooms;
int secondsInADay = HOURS_IN_A_DAY * MINUTES_IN_AN_HOUR * SECONDS_IN_A_MINUTE;
Class và đối tượng nên có tên là danh từ hoặc cụm danh từ như Customer
, WikiPage
, Account
. Hạn chế các từ như Manager
, Processor
, Data
hoặc Info
khi đặt tên class và đối tượng.
Tên hàm nên được đặt bằng động từ hoặc cụm động từ như postPayment
, deletePage
hoặc save
.
Khi overload constructor, sử dụng hàm static với tên thể hiện mối liên hệ với tham số. Ví dụ:
Complex fulcrumPoint = Complex.FromRealNumber(23.0);
sẽ tốt hơn là:
Complex fulcrumPoint = new Complex(23.0);
Hàm phải ngắn hết mức có thể. Một hàm lý tưởng viết không quá 20 dòng. Về nguyên tắc, nếu hàm quá dài hãy chia nhỏ thành các hàm con, mỗi hàm con thực hiện một việc duy nhất. Tên của hàm phải thể hiện rõ tác dụng duy nhất của nó.
public static String renderPage(PageData pageData, boolean isSuite) {
if (isTestPage(pageData))
includeSetupAndTeardownPages(pageData, isSuite);
return pageData.getHtml();
}
Số lượng tham số lý tưởng của một hàm không nên quá 2. Khi số lượng tham số nhiều, xem xét việc đóng gói các tham số liên quan thành class thích hợp. Ví dụ:
Circle makeCircle(double x, double y, double radius);
Circle makeCircle(Point center, double radius);
Không nên sử dụng tham số ra (output argument), điều này sẽ gây sự mập mờ khó hiểu. Vì chúng ta đã quen với việc truyền thông tin vào hàm qua tham số và nhận lại kết quả qua return
.
Không có tác dụng phụ
Hàm không nên thực hiện bất kỳ một việc nào khác ngoài nội dung thể hiện qua tên của nó. Như thế sẽ gây những lỗi khó hiểu do hàm thực hiện các hành vi ngoài hiểu biết của người dùng.
Hàm checkPassword
dưới đây vi phạm điều này, bởi vì nó gọi Session.initialize()
. Từ tên hàm chúng ta chỉ biết nó kiểm tra password, không hề có một gợi ý nào là nó sẽ khởi tạo session cả.
public class UserValidator {
private Cryptographer cryptographer;
public boolean checkPassword(String userName, String password) {
User user = UserGateway.findByName(userName);
if (user != User.NULL) {
String codedPhrase = user.getPhraseEncodedByPassword();
String phrase = cryptographer.decrypt(codedPhrase, password);
if ("Valid Password".equals(phrase)) {
Session.initialize();
return true;
}
}
return false;
}
}
Có thể bạn sẽ muốn sửa lại tên hàm thành checkPasswordAndInitializeSession
, tuy nhiên rõ ràng điều này vi phạm nguyên tắc mỗi hàm chỉ thực hiện duy nhất một việc.
Mỗi hàm chỉ nên thực hiện hành động hoặc trả lời câu hỏi. Thực hiện cả hai sẽ gây khó hiểu cho người đọc. Ví dụ:
public boolean set(String attribute, String value);
Hàm này thực hiện set value cho một thuộc tính, trả về true
nếu thành công và false
nếu thất bại. Điều này sẽ dẫn đến cách dùng dễ gây nhầm lẫn về ý đồ như sau:
if (set("username", "unclebob"))...
Luôn tách các đoạn code giống nhau thành các hàm riêng biệt để có thể dễ dàng sử dụng lại và bảo trì.
Một trong những động cơ để viết comment là do viết code không tốt. Khi bạn viết một module nào đó và thấy nó khó hiểu và không có tổ chức. Thế nên bạn tự nói với chính mình "À mình phải comment phần này cho dễ hiểu hơn". Đừng làm thế, tốt hơn hết là hãy viết lại code.
Code rõ ràng, dễ hiểu với ít comment tốt hơn nhiều so với code phức tạp, rối rắm với nhiều comment. Thay vì mất thời gian viết comment giải thích code, hãy dành thời gian viết lại code.
Ví dụ, viết
if (employee.isEligibleForFullBenefits()) {}
thay vì
// Check to see if the employee is eligible for full benefits
if ((employee.flags & HOURLY_FLAG) && (employee.age > 65)) {}
Thay comment bằng các hàm và tên biến thể hiện rõ ý đồ bạn muốn.
Có những lúc bạn thấy một đoạn code nào đó không dùng đến nữa, bạn comment đoạn đó đi vì nghĩ sau này có thể dùng lại. Đừng làm thế, hãy xoá nó đi, sử dụng các source control như Git thì bạn có thể xem lại lịch sử bất kỳ lúc nào. Đoạn comment đó chỉ làm việc đọc code thêm rối rắm.
Hầu như code đều được đọc từ trái sang phải và từ trên xuống dưới. Mỗi dòng thể hiện một diễn đạt nhất định, mỗi nhóm các dòng thế hiện một suy nghĩ, một sự liên quan nhất định đến nhau. Các suy nghĩ đó nên được tách biệt với nhau bằng một khoảng trắng.
Ví dụ
package fitnesse.wikitext.widgets;
import java.util.regex.*;
public class BoldWidget extends ParentWidget {
public static final String REGEXP = "'''.+?'''";
private static final Pattern pattern = Pattern.compile("'''(.+?)'''",
Pattern.MULTILINE + Pattern.DOTALL
);
public BoldWidget(ParentWidget parent, String text) throws Exception {
super(parent);
Matcher match = pattern.matcher(text);
match.find();
addChildWidgets(match.group(1));
}
public String render() throws Exception {
StringBuffer html = new StringBuffer("<b>");
html.append(childHtml()).append("</b>");
return html.toString();
}
}
sẽ rõ ràng hơn là
package fitnesse.wikitext.widgets;
import java.util.regex.*;
public class BoldWidget extends ParentWidget {
public static final String REGEXP = "'''.+?'''";
private static final Pattern pattern = Pattern.compile("'''(.+?)'''",
Pattern.MULTILINE + Pattern.DOTALL
);
public BoldWidget(ParentWidget parent, String text) throws Exception {
super(parent);
Matcher match = pattern.matcher(text);
match.find();
addChildWidgets(match.group(1));
}
public String render() throws Exception {
StringBuffer html = new StringBuffer("<b>");
html.append(childHtml()).append("</b>");
return html.toString();
}
}
Các thành phần có mối liên quan mật thiết với nhau nên được đặt gần nhau. Tránh trường hợp bạn phải chạy từ file này sang file khác, cuộn lên cuộn xuống chỉ để xem các hàm gọi đến nhau như thế nào.
Biến nên được khai báo gần vị trí sử dụng hết mức có thể. Vì hàm rất ngắn nên khai báo có thể đặt ở đầu hàm.
Các thuộc tính của class nên được khai báo ở đầu class để đảm bảo nhất quán.
Nếu một hàm gọi một hàm khác, chúng nên nằm gần nhau. Hàm được gọi nằm ngay dưới hàm gọi. Điều này giúp cho việc tìm kiếm hàm và đọc hiểu chương trình dễ hơn, tự nhiên hơn.
Ví dụ:
public class WikiPageResponder implements SecureResponder {
protected WikiPage page;
protected PageData pageData;
protected String pageTitle;
protected Request request;
protected PageCrawler crawler;
public Response makeResponse(FitNesseContext context, Request request) throws Exception {
String pageName = getPageNameOrDefault(request, "FrontPage");
loadPage(pageName, context);
if (page == null)
return notFoundResponse(context, request);
else
return makePageResponse(context);
}
private String getPageNameOrDefault(Request request, String defaultPageName) {
String pageName = request.getResource();
if (StringUtil.isBlank(pageName))
pageName = defaultPageName;
return pageName;
}
protected void loadPage(String resource, FitNesseContext context) throws Exception {
WikiPagePath path = PathParser.parse(resource);
crawler = context.root.getPageCrawler();
crawler.setDeadEndStrategy(new VirtualEnabledPageCrawler());
page = crawler.getPage(context.root, path);
if (page != null)
pageData = page.getData();
}
private Response notFoundResponse(FitNesseContext context, Request request) throws Exception {
return new NotFoundResponder().makeResponse(context, request);
}
private SimpleResponse makePageResponse(FitNesseContext context) throws Exception {
pageTitle = PathParser.render(crawler.getFullPath(page));
String html = makeHtml(context);
SimpleResponse response = new SimpleResponse();
response.setMaxAge(0);
response.setContent(html);
return response;
}
}
Số ký tự tối đa trên mỗi dòng nên được giới hạn, thông thường bé hơn 120 ký tự.
Đối với các mệnh đề phải được sắp xếp theo một thứ tự nhất định để đảm bảo hoạt động đúng, sử dụng các biến và tham sổ hàm, cũng như cách đặt tên phù hợp để thể hiện mối tương quan:
data = ReadData();
results = CalculateResultsFromData( data );
PrintResults( results );
Ở đây, data
phải được đọc trước khi xử lý sinh ra results
, và results
phải được tính toán trước khi in ra màn hình. Ba mệnh đề này có mối liên quan chặt chẽ và bắt buộc nó phải được tổ chức theo một thứ tự nhất định.
Ngược lại trong ví dụ sau, mối liên quan thứ tự đã bị ẩn đi, khiến người gọi có thể bị nhầm lẫn:
revenue.ComputeMonthly();
revenue.ComputeQuarterly();
revenue.ComputeAnnual();
Việc tính lợi nhuận theo năm phụ thuộc vào lợi nhuận theo quý đã được tính trước, và lợi nhuận theo quý phụ thuộc vào lợi nhuận theo tháng. Tuy nhiên không có cơ chế nào để bắt người sử dụng gọi các hàm này theo đúng thứ tự.
Đối với các mệnh đề mà thứ tự của chúng không ảnh hưởng đến logic, cần tổ chức sao cho việc tìm kiếm thông tin và đọc hiểu thông tin là dễ nhất. Nguyên tắc là sắp xếp code sao cho chương trình có thể đọc mượt mà liên tục từ trên xuống dưới, không cần nhảy linh tinh, cuộn lên cuộn xuống để xem hàm này cài đặt như thế nào, biến này khai báo ở đâu...
Xét ví dụ sau:
MarketingData marketingData;
SalesData salesData;
TravelData travelData;
travelData.ComputeQuarterly();
salesData.ComputeQuarterly();
marketingData.ComputeQuarterly();
salesData.ComputeAnnual();
marketingData.ComputeAnnual();
travelData.ComputeAnnual();
salesData.Print();
travelData.Print();
marketingData.Print();
Nhìn qua thì có vẻ code được tổ chức rất gọn gàng và logic. Nhưng giả sử chúng ta muốn biết marketingData
được tính như thế nào. Bạn phải đi từ dòng cuối cùng và duyệt lên hết đến đầu mã nguồn, ghi nhớ xem ở từng vị trí dữ liệu được xử lý ra làm sao, rất khó khăn.
Tổ chức code lại sao cho các mệnh đề liên quan được gom nhóm với nhau sẽ cải thiện việc đọc hiểu rất nhiều.
MarketingData marketingData;
marketingData.ComputeQuarterly();
marketingData.ComputeAnnual();
marketingData.Print();
SalesData salesData;
salesData.ComputeQuarterly();
salesData.ComputeAnnual();
salesData.Print();
TravelData travelData;
travelData.ComputeQuarterly();
travelData.ComputeAnnual();
travelData.Print();
Tổ chức sao cho trường hợp bình thường được xử lý trước, sau đó mới đến các trường hợp ngoại lệ. Ví dụ sau vi phạm quy tắc này:
OpenFile( inputFile, status )
If ( status = Status_Error ) Then
errorType = FileOpenError
Else
ReadFile( inputFile, fileData, status )
If ( status = Status_Success ) Then
SummarizeFileData( fileData, summaryData, status )
If ( status = Status_Error ) Then
errorType = ErrorType_DataSummaryError
Else
PrintSummary( summaryData )
SaveSummaryData( summaryData, status )
If ( status = Status_Error ) Then
errorType = ErrorType_SummarySaveError
Else
UpdateAllAccounts()
EraseUndoFile()
errorType = ErrorType_None
End If
End If
Else
errorType = ErrorType_FileReadError
End If
End If
Code này rất khó đọc vì phần xử lý lỗi và phần xử lý bình thường lẫn với nhau, rất khó có thể đọc được xem trong trường hợp bình thường thì code được chạy như thế nào.
Đoạn code sau đây viết lại theo cách dễ hiểu hơn:
OpenFile( inputFile, status )
If ( status = Status_Success ) Then
ReadFile( inputFile, fileData, status )
If ( status = Status_Success ) Then
SummarizeFileData( fileData, summaryData, status )
If ( status = Status_Success ) Then
PrintSummary( summaryData )
SaveSummaryData( summaryData, status )
If ( status = Status_Success ) Then
UpdateAllAccounts()
EraseUndoFile()
errorType = ErrorType_None
Else
errorType = ErrorType_SummarySaveError
End If
Else
errorType = ErrorType_DataSummaryError
End If
Else
errorType = ErrorType_FileReadError
End If
Else
errorType = ErrorType_FileOpenError
End If
Đối với các mệnh đề if-then-else liên tục nhau, xét ví dụ:
if ( inputCharacter < SPACE ) {
characterType = CharacterType_ControlCharacter;
}
else if (
inputCharacter == ' ' ||
inputCharacter == ',' ||
inputCharacter == '.' ||
inputCharacter == '!' ||
inputCharacter == '(' ||
inputCharacter == ')' ||
inputCharacter == ':' ||
inputCharacter == ';' ||
inputCharacter == '?' ||
inputCharacter == '-'
) {
characterType = CharacterType_Punctuation;
}
else if ( '0' <= inputCharacter && inputCharacter <= '9' ) {
characterType = CharacterType_Digit;
}
else if (
( 'a' <= inputCharacter && inputCharacter <= 'z' ) ||
( 'A' <= inputCharacter && inputCharacter <= 'Z' )
) {
characterType = CharacterType_Letter;
}
Thay thế các mệnh đề test phức tạp bằng các hàm tương ứng:
if ( IsControl( inputCharacter ) ) {
characterType = CharacterType_ControlCharacter;
}
else if ( IsPunctuation( inputCharacter ) ) {
characterType = CharacterType_Punctuation;
}
else if ( IsDigit( inputCharacter ) ) {
characterType = CharacterType_Digit;
}
else if ( IsLetter( inputCharacter ) ) {
characterType = CharacterType_Letter;
}
Đưa trường hợp phổ biến lên trước, ví dụ nếu chữ cái thông thường là phổ biến nhất thì ta tổ chức lại như sau:
if ( IsLetter( inputCharacter ) ) {
characterType = CharacterType_Letter;
}
else if ( IsPunctuation( inputCharacter ) ) {
characterType = CharacterType_Punctuation;
}
else if ( IsDigit( inputCharacter ) ) {
characterType = CharacterType_Digit;
}
else if ( IsControl( inputCharacter ) ) {
characterType = CharacterType_ControlCharacter;
}
Đảm bảo rằng tất cả các trường hợp đều được xử lý, kể cả những trường hợp bạn không có ý định cài đặt:
if ( IsLetter( inputCharacter ) ) {
characterType = CharacterType_Letter;
}
else if ( IsPunctuation( inputCharacter ) ) {
characterType = CharacterType_Punctuation;
}
else if ( IsDigit( inputCharacter ) ) {
characterType = CharacterType_Digit;
}
else if ( IsControl( inputCharacter ) ) {
characterType = CharacterType_ControlCharacter;
}
else {
DisplayInternalError( "Unexpected type of character detected." );
}
Mệnh đề case
Giữ cho phần code trong mỗi case ngắn gọn. Nếu có thể hay thay thế nó bằng một hàm nào đó.
action = userCommand[ 0 ];
switch ( action ) {
case 'c':
Copy();
break;
case 'd':
DeleteCharacter();
break;
case 'f':
Format();
break;
case 'h':
Help();
break;
...
default:
HandleUserInputError( ErrorType.InvalidUserCommand );
}
Một số lời khuyên khi sắp xếp thứ tự của các case:
Kết thúc mỗi case bắt buộc phải có break, trong trường hợp thật sự cần thiết phải bỏ qua thì bạn cần comment lại rõ ràng lý do tại sao cần code như thế:
switch ( errorDocumentationLevel ) {
case DocumentationLevel_Full:
DisplayErrorDetails( errorNumber );
// FALLTHROUGH -- Full documentation also prints summary comments
case DocumentationLevel_Summary:
DisplayErrorSummary( errorNumber );
// FALLTHROUGH -- Summary documentation also prints error number
case DocumentationLevel_NumberOnly:
DisplayErrorNumber( errorNumber );
break;
default:
DisplayInternalError( "Internal Error 905: Call customer support." );
}
Hai phần vừa qua chỉ là tóm tắt một phần rất nhỏ mà mình nghĩ là đối với một developer có thể nắm bắt và áp dụng ngay được. Ngoài ra còn rất nhiều nội dụng hay mà các bạn có thể tìm hiểu thêm thông qua hai cuốn sách Clean Code và Code Complete
Chúc các bạn thành công!
Via Kipalog