Functions trong clean code (Phần 1)

24/11/2020 - lượt xem
Chia sẻ
 
Rate this post

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.

Cùng xem ví dụ và nghiên cứu.

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.

 

 

 

 

Small! Function nên viết thực sự nhỏ.

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();
}

Khối code và thụt dòng (Block and indenting )

Đ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. 

Đơn chức năng (Do One Thing)

Như ví dụ về HtmlUtil ở trên ta thấy hàm thực hiện các việc như sau:

  • Xác định xem trang có phải là trang Test hay không?
  • Thiết lập và phân tách các trang con
  • Hiển thi trang bằng HTML

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 thingmulti 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

Mỗi hàm duy trì ở 1 cấp độ trừu tượng hóa (One Level of Abstraction per function )

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. 

Đọc Code từ trên xuống dưới (Reating Code from top to bottom: The Stepdown Rule )

Đ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”.

Câu lệnh switch

Bản chất.

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.

Phân tích và xử lý

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 đề: 

  • Hàm này khá lớn, đặc biệt khi mỗi loại nhân viên mới được add vào, hàm sẽ bị mở rộng ra.
  • Hàm thực hiện nhiều hơn 1 thứ
  • Vi phạm nguyên tắc Đơn chức năng  trong SOLID (Single Responsibility Principle).
  • Vi phạm nguyên tắc đóng mở ( Open Closed Principle) bởi nó sẽ thay đổi bất kì khi nào có thêm loại nhân viên mới.

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.

Sử dụng tên mô tả (Use Descriptive Names )

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ủ đề:

    Liên hệ với chúng tôi

    Để lại thông tin để nhận được các bài viết khác

    Rate this post

    Xem thêm nhiều bài tin mới nhất về Clean code

    Xem thêm