+3

Clean code - P2

Tiếp nối chương 1, chương 2, nay mình sẽ tóm tắt tiếp chương 3 cuốn 'The art of readable code'.

Chương 3 - Functions

Functions là đơn vị tổ chức nên chương trình ứng dụng. Sẽ rất khó để hiểu những function dài với các lệnh if lồng nhau, các vòng lặp for/while lồng ngồng, những khái niệm trừu tượng, các công thức tính toán khó hiểu và các chức năng khác được gọi lồng vào nữa. Trong khi chỉ cần chú ý một chút thì chúng ta lại có thể viết code tốt hơn.

Cuốn Clean code đưa ra một số tip dưới đây:

Small

Nguyên tắc đầu tiên của functions là chúng cần phải nhỏ. Nguyên tắc thứ hai là nhỏ hơn nữa. Không có công trình nào chứng minh là functions nhỏ thì tốt hơn, nhưng dựa vào kinh nghiệm của các lập trình viên trong nhiều thập kỷ đều đồng tình rằng những functions vài trăm dòng code thường gây ra sự khó chịu với người đọc code. Kinh nghiệm từ các sai lầm chỉ ra rằng functions nên được thiết kế nhỏ.

Blocks and Indenting Những khối lệnh if/else, while, for nên tách ra thành các function nhỏ xử lý và đặt thành một lời gọi hàm. Với tên các function tường minh sẽ cho biết thông tin cụ thể nó làm gì, xử lý gì khi được gọi trong một hàm lớn.

Đôi khi những function nhỏ chỉ vài ba dòng code, nhưng nếu tách ra làm code rõ nghĩa hơn thì cũng nên tách.

Do one thing

Function chỉ nên thực hiện xử lý duy nhất một điều gì đấy. Nó nên thực hiện điều đó cho tốt.

Nghe có vẻ trừu tượng??? Vấn đề ở đây là cần phân biệt one thingmulti steps. Một function (one thing) không chỉ là một công đoạn mà gồm nhiều công đoạn khác nhau (multi steps)

Giả sử có một chức năng là tính toán tài khoản người dùng chơi chứng khoán tại thời điểm hiện tại. Khi đó sẽ cần tính số dư hiện tại, số tiền lãi lỗ hiện tại,...

Thì ở đây "one thing" chính là tính toán tài khoản người dùng tại thời điểm hiện tại, còn "multi steps" là các bước tính số dư hiện tại, số tiền lãi lỗ hiện tại,......

Mỗi step hay mỗi công đoạn này lại có thể là một function con (function con này sẽ là một step trong function lớn). Khi phân rã một khái niệm lớn thành các khải niệm nhỏ sẽ dễ hiểu, dễ theo dõi hơn.

One Level of Abstraction per Function

Đây là một khái niệm khó, tự nhận thấy mình mới hiểu ở một khía cạnh nhỏ. Vì vậy chưa thể giải thích cặn cẽ, mọi người có thể tham khảo thêm ở đây để hiểu thêm về SLA (single level abstraction): http://principles-wiki.net/principles:single_level_of_abstraction https://crmbusiness.wordpress.com/2015/09/17/understanding-levels-of-abstraction/

Một ví dụ nhỏ:

public List<ResultDto> buildResult(Set<ResultEntity> resultSet) {
    List<ResultDto> result = new ArrayList<>();
    for (ResultEntity entity : resultSet) {
        ResultDto dto = new ResultDto();
        dto.setShoeSize(entity.getShoeSize());        
        dto.setNumberOfEarthWorms(entity.getNumberOfEarthWorms());
        dto.setAge(computeAge(entity.getBirthday()));
        result.add(dto);
    }
    return result;
}

Trong phương thức này sử dụng 2 mức trừu tượng. Đầu tiên là vòng lặp hoạt động trên toàn bộ tập kết quả, thứ hai là các phần tử trong trong tập kết quả chuyển đổi thành một đối tượng khác gọi là DTO. Ta có thể viết lại như sau:

public List<ResultDto> buildResult(Set<ResultEntity> resultSet) {
    List<ResultDto> result = new ArrayList<>();
    for (ResultEntity entity : resultSet) {
        result.add(toDto(entity));
    }
    return result;
}
 
private ResultDto toDto(ResultEntity entity) {
    ResultDto dto = new ResultDto();
    dto.setShoeSize(entity.getShoeSize());        
    dto.setNumberOfEarthWorms(entity.getNumberOfEarthWorms());
    dto.setAge(computeAge(entity.getBirthday()));
    return dto;
}

Việc tách ra một phương thức mới tên là 'toDto' chỉ có một mức trừu tượng là convert object, và mức trừu tượng này được gọi trong một phương thức khác rõ ràng đã giúp code clear hơn.

Use Descriptive Names

Dùng những từ ngữ dễ đọc, tường minh để đặt tên hàm và dùng những từ khóa để mô tả chức năng của nó làm gì. Một cái tên mô tả dài tốt hơn là một tên ngắn bí ẩn, không clear, và cũng tốt hơn là phải viết thêm một đoạn comment mô tả.

Function Arguments

Nhóm tác giả đưa ra ý kiến rằng một function không nên có quá 3 tham số. Khi nhiều tham số sẽ khó khăn hơn khi đứng ở góc độ kiểm thử. Sẽ rất khó khăn khi viết testcase phải đảm bảo tất cả tổ hợp các tham số đều hoạt động đúng.Nếu với môt, hai tham số thì là bình thường, nhưng nếu nhiều hơn hai tham số thì đó quả là vấn đề.

  • Common monadic function (có hai hình thức phổ biến khi sử dụng một tham số)
    Một hàm lý luận trả về true hoặc false
    Một hàm lập luận, biến nó thành một cái gì khác và trả về cái được biến đổi.
    Một hình thức khác ít phổ biến hơn là hàm xử lý sự kiện, có một tham số đầu vào nhưng không có đối số đầu ra.

  • Flag arguments
    Một hàm nhận vào tham số là một flag arguments là một hàm tồi, bời vì nó đã làm nhiều hơn một điều. Một là khi flag có giá trị true, một là khi flag có giá trị false. Nhóm tác giả cho rằng nên tách trường hợp này thành hai hàm.

  • Arguments objects
    Để ý rằng khi một số hàm cần nhiều hơn 3 tham số, có khả năng là một số trong những tham số đó có thể gói gọn trọng một object riêng của chúng.
    Circle makeCircle(double x, double y, double radius);
    Circle makeCircle(Point center, double radius);

  • Verbs and Keywords
    Mã hóa tên các tham số vào tên hàm, điều này giảm nhẹ vấn đề phải nhớ thứ tự các tham số. Ví dụ:

getCustomer(String customerId);  -> getCustomerById(String customerId);
assertEquals(Object expected, Object actual);  -> assertExpectedEqualsActual(Object expected, Object actual);

Command Query Separation

Hàm nên rõ ràng trong việc thực hiện một điều gì đó hoặc trả về một cái gì đó. Chằng hạn một hàm có chức nằng thay đổi trạng thái của đối tượng hoặc trả về thông tin của đối tượng là 2 việc khác nhau. Hàm làm cả lúc hai việc sẽ dẫn đến sự nhầm lẫn hoặc gây khó hiểu. Ví dụ

public boolean set(String attribute, String value);

Hàm này đặt giá trị của một thuộc tính và trả về true nếu thành công, false nếu thuộc tính không tồn tại.

if (set("username", "unclebob"))...

Vấn đề: Không biết hàm set này đang thực hiện theo cách nào

  • Kiểm tra thuộc tính "username" đã tồn tại hay chưa
  • Set thuộc tính "username" với giá trị "unclebob"... => Gây nhầm lẫn

Giải quyết vấn đề bằng cách đặt lại tên và phân tách các phần để sự mơ hồ không xảy ra:

if (attributeExists("username")) {
   setAttribute("username", "unclebob");
   ...
}

Sau khi viết lại, code trong đã rõ ràng và clear hơn.

Prefer Exceptions to Returning Error Codes

Trả về mã lỗi ở các phương thức là một điều không tốt. Ví du :

if (deletePage (page) == E_OK)

Điều này dẫn đến các cấu trúc lồng nhau. Khi ta trả về một mã lỗi, thì nơi gọi phương thức này sẽ phải xử lý ngay tức khắc.

if (deletePage(page) == E_OK) {
    if (registry.deleteReference(page.name) == E_OK) {
        if (configKeys.deleteKey(page.name.makeKey()) == E_OK){
            logger.log("page deleted");
        } else {
            logger.log("configKey not deleted");
        }
    } else {
        logger.log("deleteReference from registry failed");
    }
} else {
    logger.log("delete failed");
    return E_ERROR;
}

Trong khi nếu sử dụng ngoại lệ để thay thế một đoạn code lỗi, sau đó lỗi xử lý code được tách ra (lỗi ở đâu xử lý đến đó) sẽ không ảnh hưởng đến vấn đề khác.

try {
   deletePage(page);
   registry.deleteReference(page.name);
   configKeys.deleteKey(page.name.makeKey());
}
catch (Exception e) {
   logger.log(e.getMessage());
}

Tách các khổi try/catch

public void delete(Page page) {
   try {
      deletePageAndAllReferences(page);
   }
   catch (Exception e) {
      logError(e);
   }
}

private void deletePageAndAllReferences(Page page) throws Exception {
   deletePage(page);
   registry.deleteReference(page.name);
   configKeys.deleteKey(page.name.makeKey());
}

private void logError(Exception e) {
   logger.log(e.getMessage());
}

  • Error handling is one thing
    Như đã nói trước đó function chỉ nên làm một việc (do one thing). Xử lý lỗi cũng là một việc. Vậy nên một chức năng xử lý lỗi không nên làm bất cứ điều gì khác nữa.

Don't repeat yourself

Sự trùng lặp mã code là một vấn đề phổ biến. Nó làm tăng khối lượng code cũng như effort quản lý source mỗi khi có yêu cầu thay đổi.

How do you write functions?

Viết code liệu có phải cũng như viết văn, viết bài luận, viết báo cáo.... Khi viết một bài văn, hay một bài báo, ban đầu sẽ là các xây khung với các ý tưởng (lập dàn ý), sau đó phác thảo uốn nắn những dòng code để dễ đọc dễ hiểu. Việc phác thảo đầu tiên có thể vụng về, thiếu tổ chức và chưa clear. Sau đó cần review lại, sắp xếp lại xem có thể tách hàm không, tên hàm, tên biến, tên tham số đã clear chưa, ....


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí