CHƯƠNG 5: FORMATTING
Việc áp dụng format chuẩn cho hiển thị mã nguồn không chỉ giúp cho người đọc cảm thấy thoải mái dễ chịu khi đọc code, mà còn giúp bạn trở nên chuyên nghiệp hơn trong phong cách làm việc.
Trên tất cả, việc format code giúp cho mã nguồn của bạn thực sự sạch sẽ. Và việc format này vô cùng quan trọng. Format code cũng là 1 tiêu chí quan trọng để đánh giá 1 lập trình viên chuyên nghiệp trong mắt doanh nghiệp.
Có thể bạn nghĩ rằng “làm cho chạy được” là việc tối quan trọng của 1 lập trình viên chuyên nghiệp.
Những tính năng mà bạn tạo ra ngày hôm nay có thể sẽ có các thay đổi trong phiên bản tiếp theo. Nhưng khả năng đọc của mã nguồn sẽ quyết định 1 phần việc thay đổi đó được thực hiện ra sao.
Bắt đầu với kích thước theo chiều dọc. Một tệp tin nguồn nên lớn như thế nào? Trong Java, kích cỡ file liên quan chặt ché tới kích cỡ class. Chúng ta nói về kích cỡ class khi chúng ta nói về các class. Còn hiện tại chúng ta sẽ quan tâm tới kích cỡ của file.
Xem hình dưới đây
7 Dự án được đưa ra: Junit, FitNesse, testNG, Time và Money, JDepend, Ant, Tomcat. Hình trên biểu diễn về độ dài file tối thiểu, tối đa trong mỗi dự án.
Ví dụ kích thước tệp tin trung bình trong FitNesse là khoảng 65 dòng, 1 phần 3 trong các file có khoảng 40 đến 100+ dòng. File lớn nhất trong FitNesse là 400 dòng, file nhỏ nhất là 6 dòng.
Điều này có ý nghĩa gì? Nó như khẳng định việc xây dựng các hệ thống quan trọng ( FitNesse có 50.000 dòng mã) với các tệp trung bình từ 200 dòng, tối đa 500 dòng.
Hãy nghĩ về 1 bài báo được viết tốt. Chúng ta thường sẽ đọc báo theo chiều dọc, từ trên xuống dưới. Ở ngay đầu tiên là tiêu đề với mục đích cho người đọc biết bài bào này nói về vấn đề gì. Từ đó chúng ta có thể quyết định đọc hay không.
Đoạn văn đầu tiên đưa cho chúng ta tóm tắt về nội dung của bài báo, không có những thông tin quá chi tiết, thay vào đó là các thông tin nền tảng, cơ bản ban đầu.
Càng đọc xuống dưới sẽ càng là các nội dung chi tiết của bài viết.
Chúng ta cũng mong muốn nằng mã nguồn mình viết ra giống như những bài báo vậy. Tên nên thật đơn giản nhưng rõ nghĩa. Chỉ cần đọc tên thôi chúng ta có thể biết được rằng mình có đang xem đúng module hay không?
Phần trên cùng của file code là các khái niệm và thuật toán. Chi tiết sẽ tăng dần khi đọc xuống cuối file, và đến cuối cùng file chúng ta sẽ thấy các function ở mức độ chi tiết thấp nhất trong mã nguồn.
Nên chú ý rằng, 1 tờ báo thì bao gồm nhiều bài viết, đa phần chúng đều rất nhỏ, 1 vài trong số đó thì lớn hơn 1 chút. Nếu tờ báo chỉ cung cấp 1 câu chuyện kết cấu vô tổ chức với 1 mớ thông tin, chắc chắn chúng ta sẽ không đọc nó.
Hãy áp dụng điều này vào trong cách tổ chức code.
Gần như tất cả mã code đều được đọc từ trái sang phải, từ trên xuống dưới. MỖi dòng biểu thị cho 1 biểu thức hoặc 1 mệnh đề. Mỗi hàm, nhóm code đại diện cho 1 ý nghĩa (mục đich) hoàn chỉnh. Những dòng suy nghĩ này nên cách biệt nhau bởi dòng trắng.
Quy tắc cách dòng trắng này dù đơn giản nhưng có ảnh hưởng sâu sắc đến bố cục trực quan của mã. MỖi dòng trống là 1 dấu hiệu trực quan xác định rằng một khái niệm mới và riêng biệt. Đối với 1 người đọc, khi nhìn từ trên xuống dưới, chúng ta sẽ bị thu hút vào dòng đầu tiên theo sau 1 dòng trống.
So sánh 2 đoạn mã sau đây
BoldWidget.java 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(); } }
Và đoạn mã
BoldWidget.java 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(); } }
Theo khái niệm này nghĩa là các dòng code có sự liên quan chặt chẽ tới nhau sẽ xuất hiện dày đặc theo chiều dọc.
So sánh 2 ví dụ dưới đây
public class ReporterConfig { /** * The class name of the reporter listener */ private String m_className; /** * The properties of the reporter listener */ private List < Property > m_properties = new ArrayList < Property > (); public void addProperty(Property property) { m_properties.add(property); }
Và ví dụ
public class ReporterConfig { private String m_className; private List < Property > m_properties = new ArrayList < Property > (); public void addProperty(Property property) { m_properties.add(property); }
Ví dụ bên dưới ẽ dễ dàng đọc hơn. Đoạn mã được vừa vặn trong 1 khung nhìn của mắt. Chúng ta có thể nhìn thấy đoạn mã gồm 2 biến, 1 phương thức mà không cần di chuyển đầu hay mắt nhiều.
Với ví dụ trước đó, có thể chúng ta sẽ phải sử dụng mắt nhiều hơn, hay chuyển động của đầu để có được độ hiểu ở mức tương đương.
Đã bao giờ khi bạn cố gắng đọc hiểu 1 mã nguồn, mà liên tục phải di chuyển từ hàm này qua hàm khác, lăn chuột lên xuống liên tục. Dường như toàn bộ tâm trí của mình đang chỉ để tập trung vào việc ghi nhớ vị trí mà các mảnh ghép nên hệ thống đó nằm ở đâu ( đoạn nào, dòng nào)
Quy tắc này liên kết chặt chẽ với việc giữ các phần liên quan ở gần nhau bên trên. Và quy tắc này cũng không nên áp dụng cho việc các file mã nguồn tách biệt.
Biến nên được khai báo gần với nơi sử dụng chúng. Bởi vì mỗi hàm của chúng ta rất ngắn, các biết local nên xuất hiện ở đầu mỗi function.
private static void readPreferences() { InputStream is = null; try { is = new FileInputStream(getPreferencesFile()); setPreferences(new Properties(getPreferences())); getPreferences().load(is); } catch (IOException e) { try { if (is != null) is.close(); } catch (IOException e1) {} } }
Các biến sử dụng trong vòng lặp nên khai báo trong vòng lặp
public int countTestCases() { int count = 0; for (Test each: tests) count += each.countTestCases(); return count; }
Các biến dạng này nên khai báo ở đầu mỗi class.
Nếu 1 function gọi 1 function khác, chúng nên được xếp dọc gần nhau, hàm được gọi nên ở bên dưới hàm gọi. Điều này giúp cho chương trình giống như 1 dòng chảy tự nhiên. Việc này giúp người đọc dễ dàng tìm thấy hàm gọi và thuận tiện trong việc đọc hiểu chương trình đang được viết.
WikiPageResponder.java 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; }
Những khái niệm có độ giống nhau nên ở gần nhau, độ giống nhau càng cao thì càng ở gần hơn.
static public void assertTrue(String message, boolean condition) { if (!condition) fail(message); } static public void assertTrue(boolean condition) { assertTrue(null, condition); } static public void assertFalse(String message, boolean condition) { assertTrue(message, !condition); } static public void assertFalse(boolean condition) { assertFalse(null, condition); }
Những phương thức trên đây có độ giống nhau cao do chúng có cùng cách đặt tên, là các biến thể của nhau (Overload Function).
Mặc dù trong các hàm có lời gọi đến hàm khác cũng là nguyên nhân để xếp chúng gần nhau. Nhưng kể cả không có lời gọi đó, chúng vẫn nên được xếp gần nhau.
Nói chung, chúng ta nên đặt các hàm phục thuộc theo hướng từ trên xuống dưới. Nghĩa là 1 hàm được gọi phải nằm dưới 1 hàm thực hiện lời gọi. Điều này tạo ra 1 dòng chảy tự nhiên từ cao đến thấp.
Như vậy các khái nghiệm quan trọng sẽ xuất hiện trước, và các chi tiết cấp thấp nhất ở dưới cùng. Điều này có ý nghĩa trọng việc đọc lướt mã nguồn, lấy ý tưởng chính.
Dựa trên thống kê khi phân tích 1 số chương trình viết bằng java, chúng ta nhận được lời khuyên như sau
Giữ cho 1 dòng thật ngắn, như tác giả gợi ý với tầm 80 character là trung bình, và không nên vượt quá 120 character. Dù rằng ngày nay 1 màn hình có thể chứa nhiều hơn thế rất nhiều.
Chúng ta sử dụng khoảng cách chiều ngang để liên kết những thứ có độ liên quan cao và tách biệt những thứ có liên quan yếu với nhau
private void measureLine(String line) { lineCount++; int lineSize = line.length(); totalChars += lineSize; lineWidthHistogram.addLine(lineSize, lineCount); recordWidestLine(lineSize); }
Chú ý tới hàm trên đây, ví dụ như 2 vế của phép += được cách biệt bởi dấu cách, điều này làm tách biệt sự rõ ràng.
Ở 1 góc nhìn khác, giữa tên hàm và tham số thật gần nhau để thể hiện độ liên quan mật thiết của chúng với nhau. Ví dụ measureLine và dấu (
Các tham số trong hàm cũng có sự tách biệt nhau để đảm bảo sự tách biệt giữa các tham số.
Việc dóng hàng trong mã nguồn là không cần thiết. Không cần phải sắp xếp các thuộc tính được thẳng hàng với nhau.
File code cần được bố trí phân cấp, nghĩa là có cấp bậc và thứ tự. Ví dụ như class thì thường sẽ sát với lề, sau đó lùi vào 1 cấp là các function, Các phàn code xử lý nghiệp vụ chức năng sẽ lùi vào thêm 1 cấp nữa.
Việc này giúp cho người đọc dễ dàng đọc được cấu trúc nội dung file, cũng như có thể phân biệt được hàm biến 1 cách rõ ràng.
Đôi khi với các cấu trúc if, while, hay hàm ngắn chúng ta cũng có thể viết trên 1 dòng thay vì việc phải thụt lề.
Tuy vậy việc thụt lề cũng mang lại giá trị trong việc dễ dàng tiếp cận hơn
public class CommentWidget extends TextWidget { public static final String REGEXP = "^#[^\r\n]*(?:(?:\r\n)|\n|\r)?"; public CommentWidget(ParentWidget parent, String text){super(parent, text);} public String render() throws Exception {return ""; } }
sẽ được chỉnh lại như sau:
public class CommentWidget extends TextWidget { public static final String REGEXP = "^#[^\r\n]*(?:(?:\r\n)|\n|\r)?"; public CommentWidget(ParentWidget parent, String text) { super(parent, text); } public String render() throws Exception { return ""; } }
Đôi khi phần thân của while hay for chỉ là đoạn mã tượng trưng ( không thật, không có) như dưới đây
while (dis.read(buf, 0, readBufferSize) != -1) ;
Việc này không sai nhưng chúng ta nên hạn chế chúng. Nếu không thể hạn chế chúng thì nên sử dụng đóng mở ngoặc ({}) để sử dụng thay vì viết hết trên 1 dòng.
Việc viết trên 1 dòng quá khó để nhìn thấy, việc này có thể tạo ra sự nhầm lẫn trong quá trình code.
Mỗi cá nhân đều có một 1 phong cách định dạnh riêng. Tuy vậy khi làm việc nhóm việc này cần phải loại bỏ. Cả 1 đội nhóm nên thống nhất 1 định dạng chung, việc này tạo ra sự nhất quán trong q chương trình, dễ dàng cho bảo trì mở rộng về sau.
Bình luận: