Quy tắc chung
1. Đặt tên có nghĩa
Sử dụng tên thể hiện rõ ý đồ
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 daysTên biến tốt:
int elapsedTimeInDays;
int daysSinceCreation;
int daysSinceModification;
int fileAgeInDays;Sử dụng tên có thể phát âm được, tìm kiếm được
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";
/* ... */
};Tránh mã hoá
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!Tránh mental mapping
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;Tên class
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
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);2. Hàm
Ngắn gọn và làm một việc duy nhất
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();
}Tham số
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.
Tách biệt giữa hành động và truy vấn
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"))...Don't Repeat Yourself DRY
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ì.
3. Comment
Comment không thể chữa code xấu
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 benefit
sif ((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.
Comment code không dùng
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.
4. Định dạng
Độ mở theo chiều dọc giữa các nhóm code
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("");
html.append(childHtml()).append("");
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("");
html.append(childHtml()).append("");
return html.toString();
}
}Khoảng cách giữa các thành phần liên quan
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.
Khai báo biến
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.
Các hàm phụ thuộc
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);
elsereturn 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;
}
}
Định dạng theo chiều ngang
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ự.
5. Statement
Tổ chức code theo đường thẳng
Đố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();
Mệnh đề if
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:
Sắp xếp theo thứ tự bảng chữ cái: Nếu vai trò của mỗi case là như nhau, sắp theo thứ tự này làm code dễ đọc hơn và việc tìm một case cụ thể dễ hơn.
Để các case thường dùng lên đầu tiên: Người đọc có thể tìm các case thường được thực thi nhất một cách nhanh chóng.
Để các case bình thường lên trước: Trong trường hợp bạn có một số case xử lý flow thông thường và một số case xử lý ngoại lệ, để case thông thường lên trước. Thêm comment chỉ rõ case nào là bình thường, case nào xử lý ngoại lệ.
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 commentscase DocumentationLevel_Summary:
DisplayErrorSummary( errorNumber );
// FALLTHROUGH -- Summary documentation also prints error numbercase DocumentationLevel_NumberOnly:
DisplayErrorNumber( errorNumber );
break;
default:
DisplayInternalError( "Internal Error 905: Call customer support." );
}