Lại tiếp tục với series Clean Code. Tiếp đến bài này sẽ hướng dẫn việc viết 1 hàm (function/ chức năng) một cách tốt nhất dễ hiểu nhất science nhất.
Điều hiển nhiên là, tất cả mọi chương trình đều được cấu thành lên từ những function mà thôi.
File HtmlUtil.java public static String testableHtml( PageData pageData, boolean includeSuiteSetup ) throws Exception { WikiPage wikiPage = pageData.getWikiPage(); StringBuffer buffer = new StringBuffer(); if (pageData.hasAttribute("Test")) { if (includeSuiteSetup) { WikiPage suiteSetup = PageCrawlerImpl.getInheritedPage( SuiteResponder.SUITE_SETUP_NAME, wikiPage ); if (suiteSetup != null) { WikiPagePath pagePath = suiteSetup.getPageCrawler().getFullPath(suiteSetup); String pagePathName = PathParser.render(pagePath); buffer.append("!include -setup .") .append(pagePathName) .append("\n"); } } WikiPage setup = PageCrawlerImpl.getInheritedPage("SetUp", wikiPage); if (setup != null) { WikiPagePath setupPath = wikiPage.getPageCrawler().getFullPath(setup); String setupPathName = PathParser.render(setupPath); buffer.append("!include -setup .") .append(setupPathName) .append("\n"); } } buffer.append(pageData.getContent()); if (pageData.hasAttribute("Test")) { WikiPage teardown = PageCrawlerImpl.getInheritedPage("TearDown", wikiPage); if (teardown != null) { WikiPagePath tearDownPath = wikiPage.getPageCrawler().getFullPath(teardown); String tearDownPathName = PathParser.render(tearDownPath); buffer.append("\n") .append("!include -teardown .") .append(tearDownPathName) .append("\n"); } if (includeSuiteSetup) { WikiPage suiteTeardown = PageCrawlerImpl.getInheritedPage( SuiteResponder.SUITE_TEARDOWN_NAME, wikiPage ); if (suiteTeardown != null) { WikiPagePath pagePath = suiteTeardown.getPageCrawler().getFullPath(suiteTeardown); String pagePathName = PathParser.render(pagePath); buffer.append("!include -teardown .") .append(pagePathName) .append("\n"); } } } pageData.setContent(buffer.toString()); return pageData.getHtml(); }
Trong đoạn mã trên đây ta thấy 1 function rất dài, code thì trùng lặp, nhiều chuỗi string đơn lẻ. Về cơ bản thì thực sự không dễ dàng hiểu được chức năng của function này
Tuy vậy khi ta tách nhỏ hàm này, thực hiện đổi tên và tái cấu trúc, việc nắm bắt được ý định của hàm sẽ dễ dàng hơn rất nhiều. Cùng xem đoạn code này nhé:
public static String renderPageWithSetupsAndTeardowns( PageData pageData, boolean isSuite ) throws Exception { boolean isTestPage = pageData.hasAttribute("Test"); if (isTestPage) { WikiPage testPage = pageData.getWikiPage(); StringBuffer newPageContent = new StringBuffer(); includeSetupPages(testPage, newPageContent, isSuite); newPageContent.append(pageData.getContent()); includeTeardownPages(testPage, newPageContent, isSuite); pageData.setContent(newPageContent.toString()); } return pageData.getHtml(); }
Chúng ta tuy không hiểu hết ý nghĩa của hàm nhưng về cơ bản là biết chức năng của hàm. Nó bao gồm một vài cài đặt, tách nhỏ các trang từ Test Page, cuối cùng là render ra nội dung mã html.
Nguyên tắc đầu tiên của tính năng (function) là chúng phải nhỏ.
Nguyên tắc thứ 2 là chúng cần phải nhỏ hơn nữa.
Không có bất kì nghiên cứu nào nói rằng việc này là chính xác. Tuy nhiên kết luận này dựa trên kinh nghiệm của tác giả thông qua 40 năm viết những dòng code. Tác giả đã viết những function dài vài trăm dòng, cho đến những function 20, 30 dòng. Tổng kết từ việc này là những hàm chức năng nên nhỏ gọn sẽ tốt hơn.
Vào những năm 80, thì việc viết code thường được nói rằng không nên lớn hơn 1 màn hình – khoảng 24 dòng và 80 ký tự. Ngày nay với kiểu font chữ khác, kích cỡ màn hình lớn hơn, chúng ta có thể viết lên đến 150 ký tự 1 dòng, với 100 dòng trên màn hình. Tuy vậy, 1 function thực sự cũng chỉ nên dừng lại ở khoảng 20 dòng mà thôi
Xem ví dụ về HtmlUtil khi được viết lại 1 lần nữa:
public static String renderPageWithSetupsAndTeardowns( PageData pageData, boolean isSuite) throws Exception { if (isTestPage(pageData)) includeSetupAndTeardownPages(pageData, isSuite); return pageData.getHtml(); }
Điều này ngụ ý rằng các khối bên trong các đoạn mã lệnh if, else, while nên là 1 dòng dài và trong đó nên là một lời gọi hàm. Điều này giữ cho hàm của chúng ta nhỏ gọn và còn có thể cung cấp mục đích đoạn mã đó thông qua cách đặt tên hàm một cách dễ hiểu.
Các function không nên có cấu trúc lồng nhau quá nhiều. Độ thụt dòng chỉ nên dừng lại ở mức 1 2 (tab) mà thôi. Điều này sẽ giúp các đoạn mã trở nên dễ đọc và dễ hiểu hơn rất nhiều.
Như ví dụ về HtmlUtil ở trên ta thấy hàm thực hiện các việc như sau:
Vậy hàm này là làm 1 việc hay 3 việc? Khi phân tích kĩ chúng ta thấy rằng 3 bước này thực chất có thể xét trên cùng 1 cấp dộ xử lý.
Chúng ta cần phân biệt rõ one thing và multi steps. Một function có thể bao gồm nhiều bước, công đoạn khác nhau. Mỗi bước có thể là lời gọi hàm khác. Mục tiêu của việc này là để phân rã 1 khái niệm lớn hơn thành các phần nhỏ hơn
Chúng ta cần đảm bảo rằng các câu lệnh của chúng ta được đặt cùng ở cấp độ trừu tượng hóa. Việc trộn lẫn nhiều cấp độ trong cùng 1 function có thể tạo ra những sự nhầm lẫn cho người đọc.
Việc không tuân thủ quy tắc này có thể tạo ra nhưng đoạn mã lộn xộn do có quá nhiều sự chi tiết lẫn với mục đích chính của hàm.
Đoạn mã nên diễn giải từ trên trên xuống, theo từng cấp độ trừu tượng của chúng, với độ trừu tượng của từng hàm giảm dần. Điều này gọi là nguyên tắc Stepdown.
Với nguyên tắc này thì chúng ta có thể giữ cho 1 function luôn ngắn và thỏa mãn “one thing”.
Bản chất các câu lệnh switch luôn thực hiện N thứ khác nhau, do vậy thực sự rất khó để đảm bảo lệnh switch chỉ làm 1 việc duy nhất.
Chúng ta khó có thể loại bỏ việc sử dụng câu lệnh switch, tuy nhiên chúng ta có thể đảm bảo được việc chỉ sử dụng switch trong các lớp cấp thấp và không lặp lại. Đương nhiên chúng ta xử lý việc này với việc áp dụng tính đa hình.
public Money calculatePay(Employee e) throws InvalidEmployeeType { switch (e.type) { case COMMISSIONED: return calculateCommissionedPay(e); case HOURLY: return calculateHourlyPay(e); case SALARIED: return calculateSalariedPay(e); default: throw new InvalidEmployeeType(e.type); } }
Với đoạn mã này chúng ta sẽ thấy có vài vấn đề:
Nhưng vấn đề tệ nhất đó là sẽ sinh ra các function khác có cấu trúc tương tự như function này. Việc này liên quan tới tổ chức code. Do hàm calculatePay mới chỉ thực hiện 1 chức năng là tính toán mức lương cần thanh toán. Sẽ có nhiều hàm khác với chức năng như: lấy ngày thanh toán, cách thức thay toán…
Để giải quyết vấn đề này ta sử dụng lệnh switch trong lớp tầng thấp của Abstract Factory Pattern và không 1 ai có thể thấy nó. Factory sẽ sử dụng lệnh switch để tạo ra các đối tượng Employee tương ứng, các chức năng sẽ xử lý theo cơ chế đa hình với Interface Employee.
Quy tắc chung cho việc sử dụng switch là chỉ chỉ nên xuất hiện 1 lần, được sử dụng để tạo các đối tượng đa hình và ẩn sau mối quan hệ kế thừa.
Chúng ta đã biết rằng càng tách nhỏ hàm hay mỗi hàm chỉ nên làm 1 nhiệm vụ nhất định. Thì song hành với đó, là lựa chọn những cái tên có thể mô tả cho nội dung hàm. Bản chất hàm càng nhỏ thì việc mô tả hàm đó càng dễ dàng.
Đừng sợ tốn thời gian cho việc lựa chọn tên phù hợp. Bạn có thể thử vài cái tên và đọc lại code để tìm được tên phù hợp.
Tên mô trả rõ ràng giúp chúng ta có thể hình dung rõ module, cách tổ chức đoạn mã từ đó giúp chúng ta có thể tái cấu trúc chúng.
Sử dụng tên nhastas quán trong suốt module của chúng ta.
Ví dụ như: includeSetupAndTeardownPages, includeetupPages, includeSuiteSetupPage
Tàm thời kết thúc phần một tại đây. để tìm hiểu dõ hơn hãy đọc tiếp Functions trong clean code (Phần 2)
Bài viết cùng chủ đề:
Bình luận: