Viết code nghệ thuật, để trở thành Pro - Phần 1

alt text

Bài viết tham khảo nội dung và hình ảnh từ hai cuốn sách gối đầu giường của dev, Clean CodeCode Complete, nếu có thời gian thì các bạn có thể tìm đọc thêm, rất hữu ích.

Mở đầu

Cách đây khoảng mấy chục năm, có lẽ điều quan trọng nhất khi bạn code chỉ là sự hiệu quả và tối ưu. Khi đó RAM của máy tính còn được tính bằng byte thay vì giggabyte, tài nguyên hạn hẹp đến mức mỗi lúc máy tính chỉ có thể chạy một chương trình. Những ngày đó đã xa rồi.

Cùng với tốc độ phát triển nhanh chóng của phần cứng, tính hiệu quả tụt lại phía sau và nhường chỗ cho một yếu tố quan trọng hơn, viết code đẹp, dễ đọc và dễ hiểu. Điều này xuất phát từ thực tiễn là quy mô của các project trở nên lớn hơn, các mô hình Agile đòi hỏi sự cộng tác giữa mọi người nhiều hơn, môi trường và requirement thay đổi nhanh chóng đòi hỏi code phải dễ dàng thay đổi và bảo trì.

Tuy nhiên, khi mới bắt đầu chúng ta thường có thiên hướng viết code xấu. Có thể vì lười, hoặc có thể bạn nghĩ không có đủ thời gian để làm công việc tốt hơn, rằng boss sẽ tức giận vì bạn dành quá nhiều thời gian để viết code đẹp hơn. Cũng có thể bạn quá mệt mỏi khi viết một chương trình nào đó và chỉ muốn làm cho xong thôi. Và thế là những đống code thập cẩm ra đời.

Những lúc nhìn lại đống code đó, chúng ta lại muốn để lại đến ngày hôm sau. Chúng ta thở phào nhẹ nhõm khi thấy chương trình vẫn hoạt động bình thường, và cho rằng một đống code lộn xộn mà chạy được vẫn tốt hơn là không có gì. Tất cả đều ổn cho đến ngày bug xuất hiện, hoặc khách hàng ngẫu hứng muốn thêm một vài thay đổi để ứng dụng ngầu hơn. Và mọi rắc rối bắt đầu.

Thế nào là code đẹp

Viết code đẹp đòi hỏi chúng ta phải hình thành được cảm giác về code, cảm giác về tính sạch đẹp của nó (code-sense). Có những người sinh ra đã có cảm giác này rồi, những người khác phải luyện tập và học hỏi mới đạt được. Nó không chỉ giúp chúng ta nhìn ra một đoạn code là tốt hay xấu, mà còn cho chúng ta thấy những phương pháp, cách thức để biến code xấu thành code đẹp.

Tôi muốn code của mình phải tinh tế và hiệu quả. Logic phải rõ ràng để bug không thể ẩn nấp được, sự phụ thuộc lẫn nhau giữa các thành phần được tối thiểu hoá để bảo trì dễ hơn, hiệu năng gần như tối ưu để không khiến người khác làm code lộn xộn bằng những đoạn tối ưu không có quy tắc. Code đẹp là code làm một việc tốt.

Bjarne Stroustrup, inventor of C++ and author of The C++ Programming Language

Code đẹp là code đơn giản và ý đồ rõ ràng. Code đẹp đọc như một đoạn văn hay. Code đẹp không làm mơ hồ ý đồ của người viết, ngược lại là sự kết hợp của sự trừu tượng hoá và những câu lệnh điều khiển rõ ràng.

Grady Booch, author of Object Oriented Analysis and Design with Applications

Code đẹp là code có thể đọc và cải thiện bởi những người khác. Có unit test và acceptance test. Code đẹp dùng cách đặt tên có nghĩa, cung cấp một cách xử lý duy nhất cho mỗi mục đích, sự phụ thuộc lẫn nhau được tối thiểu hoá, cung cấp API rõ ràng và đơn giản.

“Big” Dave Thomas, founder of OTI, godfather of the Eclipse strategy

Để viết code đẹp hơn

Giới thiệu dài dòng đủ rồi, sau đây chúng ta sẽ đi vào phần chính. Các quy tắc sẽ được trình bày một cách ngắn gọn nhất có thể, kèm với ví dụ.

1. Đặt tên có nghĩa

alt text

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 days

Tê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

alt text

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

alt text

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 benefits
if ((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("<b>");
    html.append(childHtml()).append("</b>");
    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("<b>");
    html.append(childHtml()).append("</b>");
    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);
    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 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ự.

Đọc tiếp

Viết code nghệ thuật, để trở thành Pro - Phần 2

Bình luận


White
{{ comment.user.name }}
Bỏ hay Hay
{{comment.like_count}}
Male avatar
{{ comment_error }}
Hủy
   

Hiển thị thử

Chỉnh sửa

White

Nguyễn Anh Tuấn

2 bài viết.
34 người follow
Kipalog
{{userFollowed ? 'Following' : 'Follow'}}
Cùng một tác giả
White
22 3
Trước khi đi vào phần chính của bài viết, chúng ta cùng điểm tâm bằng một chút design nhé. Một design tốt + code chuẩn là những gì làm nên một hệ t...
Nguyễn Anh Tuấn viết hơn 1 năm trước
22 3
{{like_count}}

kipalog

{{ comment_count }}

bình luận

{{liked ? "Đã kipalog" : "Kipalog"}}


White
{{userFollowed ? 'Following' : 'Follow'}}
2 bài viết.
34 người follow

 Đầu mục bài viết

Vẫn còn nữa! x

Kipalog vẫn còn rất nhiều bài viết hay và chủ đề thú vị chờ bạn khám phá!