Giới thiệu cuốn “The Art of Readable Code” (3)

Cuốn sách này của Dustin Boswell và Trevor Foucher tập trung vào các kỹ thuật đơn giản và hữu hiệu để viết code tốt hơn. Bạn có thể áp dụng các kỹ thuật này bất cứ khi nào bạn viết code. Tác giả trình bày các ý tưởng chính sau:

i. Cần viết code dễ hiểu

ii. Cần viết code để người đọc tốn ít thời gian nhất để hiểu.

Tác giả sử dụng các ví dụ dễ hiểu được viết trên nhiều ngôn ngữ để trình bày trong các chương các khía cạnh khác nhau để giúp bạn viết code dễ hiểu.

I. Đặt tên, chú thích và định dạng đơn giản - sử dụng cho tất cả các dòng code bạn viết

II. Giảm thiểu sự phức tạp và mập mờ trong cách dùng các vòng lặp, logic và các biến

III. Xem xét vấn đề ở mức độ hàm, như cấu trúc lại các khối code để thực hiện 1 task 1 lần

IV. Viết code để test hiệu quả, chính xác và dễ đọc.

**III. Xem xét vấn đề ở mức độ hàm, như cấu trúc lại các khối code để thực hiện 1 task 1 **

1. Trích xuất ra các “vấn đề nhỏ không liên quan” đến mục tiêu chính của chương trình

Hiểu một cách đơn giản, chương này nhằm tách các code chung chung ra khỏi code đặc trưng của dự án. Hầu hết code là code chung. Chúng ta có thể xây dựng tập lớn các thư viện và hàm helper để giải quyết các vấn đề chung và một lõi nhỏ để làm nên đặc trưng của dự án. Điều này sẽ giúp các lập trình viên tập trung vào phần lõi của dự án và có được cách viết chính xác, hiệu quả hơn. Chúng ta cũng có thể dùng lại nó về sau Ví dụ, chúng ta có một thư viện Python bao gồm các thông tin nhạy cảm như {"username": "...", "password": "..." } và cần đặt các thông tin này vào URL. Chúng ta sẽ cần rất nhiều code như

user_info = { "username": "...", "password": "..." }
user_str = json.dumps(user_info)
cipher= Cipher("aes_128_cbc", key=PRIVATE_KEY, init_vector=INIT_VECTOR, op=ENCODE)
encrypted_bytes = cipher.update(user_str)
encrypted_bytes += cipher.final() # flush out the current 128 bit block
url = "http://example.com/?user_info=" + base64.urlsafe_b64encode(encrypted_bytes)
...

Chúng ta có thể viết lại đoạn code sau như sau:

user_info = { "username": "...", "password": "..." }
url = "http://example.com/?user_info=" + url_safe_encrypt_obj(user_info)

def url_safe_encrypt_obj(obj):
  obj_str = json.dumps(obj)
  return url_safe_encrypt_str(obj_str)

def url_safe_encrypt_str(data):
  encrypted_bytes = encrypt(data)
  return base64.urlsafe_b64encode(encrypted_bytes)

def encrypt(data):
  cipher = make_cipher()
  encrypted_bytes = cipher.update(data)
  encrypted_bytes += cipher.final() # flush out any remaining bytes
  return encrypted_bytes

def make_cipher():
  return Cipher("aes_128_cbc", key=PRIVATE_KEY, init_vector=INIT_VECTOR, op=ENCODE)

2. 1 task 1 lần

Chương này giải thích một kỹ thuật đơn giản để tái cấu trúc lại code của bạn là chỉ làm 1 task 1 lần.

Nếu bạn viết code nhưng đọc rất khó hiểu, hãy thử liệt kê ra tất cả các task đang làm. Một số task có thể dễ dàng được chia thành các hàm (hoặc lớp). Một số khác trở thành các paragraphs logic với 1 hàm đơn. Việc chính xác là bạn chia thành bao nhiêu task không quan trọng bằng thực tế là chúng đã được chia tách. Ví dụ, trong một hệ thống web-crawling, bạn xây dụng hàm UpdateCounts() để thống kê các thông số khác nhau mỗi lần web page được download. Thực tế là bạn muốn có chương trình như sau:

 void UpdateCounts(HttpDownload hd) {
  counts["Exit State" ][hd.exit_state()]++; // e.g. "SUCCESS" or "FAILURE"
  counts["Http Response"][hd.http_response()]++; // e.g. "404 NOT FOUND"
  counts["Content-Type" ][hd.content_type()]++; // e.g. "text/html"
}

Nhưng thực tế HttpDownload không có các hàm ấy mà nó là một lớp rất phức tạp với rất nhiều lớp nested. Thỉnh thoảng các giá trị này còn không có, nên ta phải dùng unknown Vì thế code sẽ có dạng như sau

// WARNING: DO NOT STARE DIRECTLY AT THIS CODE FOR EXTENDED PERIODS OF TIME.
void UpdateCounts(HttpDownload hd) {
  // Figure out the Exit State, if available.
  if (!hd.has_event_log() || !hd.event_log().has_exit_state()) {
    counts["Exit State"]["unknown"]++;
  } else {
    string state_str = ExitStateTypeName(hd.event_log().exit_state());
  counts["Exit State"][state_str]++;
  }

  // If there are no HTTP headers at all, use "unknown" for the remaining elements.
  if (!hd.has_http_headers()) {
    counts["Http Response"]["unknown"]++;
    counts["Content-Type"]["unknown"]++;
    return;
  }
  HttpHeaders headers = hd.http_headers();

  // Log the HTTP response, if known, otherwise log "unknown"
  if (!headers.has_response_code()) {
    counts["Http Response"]["unknown"]++;
  } else {
    string code = StringPrintf("%d", headers.response_code());
    counts["Http Response"][code]++;
  }

  // Log the Content-Type if known, otherwise log "unknown"
  if (!headers.has_content_type()) {
    counts["Content-Type"]["unknown"]++;
  } else {
    string content_type = ContentTypeMime(headers.content_type());
    counts["Content-Type"][content_type]++;
  }
}

Ở đây, có rất nhiều code, nhiều logic và các dòng code lặp lại nên khó đọc. Cụ thể, code chuyển đi chuyển lại giữa các task

  1. Sử dụng “unknown” như giá trị default ho mỗi key.
  2. Kiểm tra xem các thành viên cảu HttpDownload có bị thiếu
  3. Trích xuất giá trị và chuyển sang string
  4. Cập nhật counts[]

Chúng ta có thể cải tiến code để tách các task.

void UpdateCounts(HttpDownload hd) {
  // Task: define default values for each of the values we want to extract
  string exit_state = "unknown";
  string http_response = "unknown";
  string content_type = "unknown";

  // Task: try to extract each value from HttpDownload, one by one
  if (hd.has_event_log() && hd.event_log().has_exit_state()) {
    exit_state = ExitStateTypeName(hd.event_log().exit_state());
  }
  if (hd.has_http_headers() && hd.http_headers().has_response_code()) {
    http_response = StringPrintf("%d", hd.http_headers().response_code());
  }
  if (h
  d.has_http_headers() && hd.http_headers().has_content_type()) {
    content_type = ContentTypeMime(hd.http_headers().content_type());
  }

  // Task: update counts[]
  counts["Exit State"][exit_state]++;
  counts["Http Response"][http_response]++;
  counts["Content-Type"][content_type]++;
}

3. Chuyển các suy nghĩ vào code

Chương này giải thích kỹ thuật đơn giản để mô tả chương trình của bạn bằng tiếng Anh và sử dụng các giải thích đó để viết code tự nhiên hơn. Kỹ thuật này rất đơn giản nhưng rất hiệu quả. Nhìn vào các giải thích bằng lời cũng có thể giúp bạn nhìn ra các vấn đề nhỏ. Ngoài ra, nếu bạn không thể mô tả vấn đề hay thiết kế của bạn bằng lời, có thể có gì đó đã bị thiếu hoặc chưa được xác định. Mô tả một chương trình, ý tưởng bằng lời chính là đã định hình cho chương trình, ý tưởng đó. Ví dụ yêu cầu khi click vào link “Show me another tip!", màn hình chuyển đến tip tiếp theo có thể được thực hiện sử dụng thư viện jQuery JavaScript

var show_next_tip = function () {
  var num_tips = $('.tip').size();
  var shown_tip = $('.tip:visible');
  var shown_tip_num = Number(shown_tip.attr('id').slice(4));
  if (shown_tip_num === num_tips) {
    $('#tip-1').show();
  } else {
    $('#tip-' + (shown_tip_num + 1)).show();
  }
  shown_tip.hide();
};

Code này OK nhưng có thể được viết tốt hơn. Hãy diễn tả bằng lời các việc cần làm

  1. Tìm tip đang hiển thị hiện tại và ẩn nó đi
  2. Sau đó tìm tip tiếp theo và hiển thị
  3. Nếu đã hiển thị tip cuối cùng thì quay lại tip đầu tiên Dựa trên diễn tả trên, ta có thể viết lại chương trình như sau
var show_next_tip = function () {
  // find the currently visible tip and hide it
  var cur_tip = $('.tip:visible').hide();

  // find the next tip after it
  var next_tip = cur_tip.next('.tip');

  // if we've run out of tips
  if (next_tip.size() === 0) {
    // cycle back to the first tip
    next_tip = $('.tip:first');
  }
  // show the new tip
  next_tip.show();
};

4. Viết code ít hơn

Chương này mô tả cách viết code ít nhất có thể. Mỗi dòng code mới cần được kiểm tra, làm documents và bảo trì. Càng viết nhiều code thì nó càng nặng hơn và khó phát triển. Bạn có thể tránh được việc viết thêm các dòng code mới bằng cách

1. Loại bỏ các feature không cần thiết

Ví dụ về xác định các store. Đầu tiên bạn có thể nghĩ phức tạp là với một tọa độ bất kỳ của user,cần tìm store gần nhát. Như thế nếu bạn muốn giải quyết chính xác 100% bạn cần suy nghĩ đến tọa độ đó có ở cực Bắc hay cực Nam, thay đổi theo đường cong của Trái đất…nhưng nếu chỉ có 30 store ở Texas thì bạn có thể giảm yêu cầu xuống tìm store gần nhất (xấp xỉ) ở Texas cho các người dùng ở gần Texas

2. Xem xét lại các yêu cầu để có cách giải quyết đơn giản nhất mà vẫn đảm bảo yêu cầu

Chúng ta có một úng dụng Java thường xuyên đọc các object từ đĩa. Để nâng cao tốc đô, chúng ta cần thực hiện cache. Đầu tiên chúng ta có thể nghĩ cách để cache loại bỏ các item ít được dùng nhất. Chúng ta phải tự thực hiện vì nó không có sẵn trong thư viện nhưng để thực hiện cấu trúc dữ liệu này chúng ta cần bảng hash, link list… có thể tốn đến 100 dòng code. Tuy nhiên, để ý rằng việc truy cập lặp lại luôn luôn ở trong 1 hàng. Nên thay vì thực hiện LRU cache, ta có thể thực hiện 1-item cache

DiskObject lastUsed; // class member
DiskObject lookUp(String key) {
  if (lastUsed == null || !lastUsed.key().equals(key)) {
    lastUsed = loadDiskObject(key);
  }
  return lastUsed;
}

Điều này giảm đến 90% lượng code và sử dụng memory nhỏ hơn.

3. Làm quen với các thư viện chuẩn bằng cách thường xuyên tìm hiểu các API.

Chúng ta có thể viết hàm unique như sau

def unique(elements):
  temp = {}
  for element in elements:
    temp[element] = None # The value doesn't matter.
    return temp.keys()
    unique_elements = unique([2,1,2])

Nhưng thực tế là chúng ta có thể dùng hàm có sẵn

unique_elements = set([2,1,2]) # Remove duplicates.

(Continues)


All Rights Reserved