+6

Refactoring techniques - Composing Methods (part 1)

Mở đầu

Có nhiều kỹ thuật refactoring được áp dụng để tái cấu trúc các hàm (phương thức) một cách chính xác. Trong hầu hết các trường hợp, các phương thức quá dài là gốc rễ của mọi vấn đề phát sinh (exception,bug... ). Những phương thức này khiến cho chương trình trở nên khó hiểu, che giấu tính logic, thứ tự thực hiện, khó khăn cho công tác bảo trì.

Các kỹ thuật tái cấu trúc trong nhóm này hợp lý hóa các phương thức, loại bỏ sự trùng lặp trong code, và mở đường cho những cải tiến trong tương lai.

1. Extract Method

1. 1 Vấn đề

Bạn có một đoạn mã có thể được nhóm lại với nhau. (tất cả code đều được tống hết vào 1 phương thức)

void printOwing() {
  printBanner();

  //print details
  System.out.println("name: " + name);
  System.out.println("amount: " + getOutstanding());
}

1. 2 Giải pháp

Di chuyển đoạn code đó sang một phương thức khác (hoặc chức năng mới) và thay thế đoạn code ở vị trí cũ bằng cuộc gọi tới phương thức mới.

void printOwing() {
  printBanner();
  printDetails(getOutstanding());
}

void printDetails(double outstanding) {
  System.out.println("name: " + name);
  System.out.println("amount: " + outstanding);
}

1. 3 Tại sao lại cần refactor

Càng tìm thấy nhiều dòng trong một phương thức, càng khó để đọc hiểu code (ví dụ trong một phương thức dài, việc tìm xem 1 chức năng cụ thể này do đoạn code nào thực hiện là 1 điều khá vất vả). Đây là lý do chính cho việc tái cấu trúc này.

Ngoài việc loại bỏ các khía cạnh gồ ghề trong code của bạn (hãy tưởng tượng bạn đang cần mài 1 con dao sắc), Extract method cũng là một bước trong nhiều phương pháp tái cấu trúc khác.

1. 4 Lợi ích

Code có thể đọc được nhiều hơn! Đảm bảo đặt cho phương thức mới một cái tên mô tả đúng mục đích của phương thức. Ví dụ: createOrder (), renderCustomerInfo (), v.v ...

Hạn chế sao chép mã. Thông thường, 1 số đoạn code được tìm thấy trong một phương thức có thể được sử dụng lại ở những nơi khác trong chương trình của bạn. Vì vậy, bạn có thể thay thế các đoạn code đó bằng các cuộc gọi đến phương thức mới của bạn.

1. 5 Làm thế nào để Refactor

  1. Tạo một phương pháp mới và đặt tên nó gợi nhớ đến chức năng mà nó thực hiện. Sao chép đoạn mã có liên quan đến phương thức mới của bạn. Xóa đoạn code khỏi vị trí cũ và thay bằng một cuộc gọi tới phương thức mới ở đó.

  2. Tìm tất cả các biến được sử dụng trong đoạn code này. Nếu chúng chỉ được khai báo bên trong đoạn code này và không được sử dụng bên ngoài, chỉ cần để chúng không thay đổi - chúng sẽ trở thành biến cục bộ cho phương thức mới.

  3. Nếu các biến được khai báo trước đoạn code mà bạn đang sử dụng, bạn sẽ cần phải khai báo các biến này là tham số đầu vào trong phương thức mới của bạn. Đôi khi, bạn có thể lấy được giá trị của các biến này một cách dễ dàng hơn bằng cách sử dụng phương pháp Replace Temp with Query.

  4. Trong phương thức mới, nếu bạn thấy 1 biến cục bộ bị thay đổi giá trị, điều này có nghĩa là giá trị thay đổi này có thể cần thiết cho phương thức chính. Hãy kiểm tra và nếu điều đó đúng, trả lại giá trị của biến này (return ) về phương thức chính để giữ mọi thứ hoạt động bình thường.

1.6 Ví dụ

Đoạn code sau đây cần refactor:

void printOwing() {
    Enumeration e = orders.elements();
    double outstanding = 0.0;
    // print banner
    System.out.println ("**************************");
    System.out.println ("***** Customer Owes ******");
    System.out.println ("**************************");
    // calculate outstanding
    while (e.hasMoreElements()) {
        Order each = (Order) e.nextElement();
        outstanding += each.getAmount();
    }
    //print details
    System.out.println ("name:" + name);
    System.out.println ("amount" + outstanding);
}

1.6.1 Không có biến cục bộ

Tôi thử áp dụng Extract Method vào phương thức này, tách riêng phần "print banner" sang một phương thức mới ta được:

void printOwing() {
    Enumeration e = orders.elements();
    double outstanding = 0.0;
    printBanner();
    // calculate outstanding
    while (e.hasMoreElements()) {
        Order each = (Order) e.nextElement();
        outstanding += each.getAmount();
    }
    //print details
    System.out.println ("name:" + name);
    System.out.println ("amount" + outstanding);
 }
 void printBanner() {
    // print banner
    System.out.println ("**************************");
    System.out.println ("***** Customer Owes ******");
    System.out.println ("**************************");
}

1.6.2 Có biến cục bộ

Tiếp tục tách riêng phần "print details " trong đoạn code mục 1.6.1 sang một phương thức mới. Lần này, phương thức mới có sử dụng tham số đầu vào. Ta có kết quả (tôi xin phép không viết lại phương thức printBanner()):

void printOwing() {
    Enumeration e = orders.elements();
    double outstanding = 0.0;
    printBanner();
    // calculate outstanding
    while (e.hasMoreElements()) {
        Order each = (Order) e.nextElement();
        outstanding += each.getAmount();
    }
    printDetails(outstanding);
}
void printDetails (double outstanding) {
    System.out.println ("name:" + name);
    System.out.println ("amount" + outstanding);
}

1.6.3 Thay đổi giá trị của biến cục bộ

Nâng cấp "max binh" phương pháp Extract Method vào vòng lặp "calculate outstanding", ta có kết quả:

void printOwing() {
    printBanner();
    double outstanding = getOutstanding();
    printDetails(outstanding);
}
double getOutstanding() {
    Enumeration e = orders.elements();
    double outstanding = 0.0;
    while (e.hasMoreElements()) {
        Order each = (Order) e.nextElement();
        outstanding += each.getAmount();
    }
    return outstanding;
}

Kết qủa chúng ta có thể thấy rõ sự thay đổi của phương thức gốc "printOwing()". Đoạn code trên vẫn có thể tối ưu hơn nhưng ví dụ trong giới hạn của phương pháp Extract Method mình sẽ dừng lại ở đây.

2. Inline Method

2. 1 Vấn đề

Khi phần thân của một phương thức cũng rõ ràng như tên của nó, hãy sử dụng kỹ thuật này.

class PizzaDelivery {
  //...
  int getRating() {
    return moreThanFiveLateDeliveries() ? 2 : 1;
  }
  boolean moreThanFiveLateDeliveries() {
    return numberOfLateDeliveries > 5;
  }
}

2. 2 Giải pháp

Thay thế các cuộc gọi đến phương thức với nội dung của phương pháp và xóa chính phương pháp.

class PizzaDelivery {
  //...
  int getRating() {
    return numberOfLateDeliveries > 5 ? 2 : 1;
  }
}

2. 3 Tại sao lại cần Refactor

Một phương thức chỉ đơn giản là gọi đến một phương thức khác, không có gì sai ở đây cả. Nhưng khi có rất nhiều phương thức như vậy, chúng làm cho code của bạn trở nên rối rắm, khó kiểm soát.

Thông thường, khi bắt đầu phát triển một chương trình, ít ai lại viết ra những phương thức như vậy. Lý do có thể là do trong quá trình phát triển, có các thay đổi xảy ra khiến cho chương trình tồn tại những phương thức như thế. Vào lúc đó, hãy mạnh dạn áp dụng phương pháp tái cấu trúc này.

2.4 Lợi ích

Bằng cách giảm thiểu số lượng các phương pháp không cần thiết, bạn sẽ làm cho code trở nên đơn giản hơn.

2. 5 Làm thế nào để Refactor

  1. Đảm bảo rằng phương thức này không được định nghĩa lại trong các lớp con. Nếu phương thức được định nghĩa lại, không nên sử dụng kỹ thuật này.
  2. Tìm tất cả các cuộc gọi tới phương thức. Thay thế các cuộc gọi này bằng nội dung của phương thức.
  3. Xóa phương thức.

3. Extract Variable

3. 1 Vấn đề

Bạn có một biểu thức rất phức tạp và khó hiểu.

void renderBanner() {
  if ((platform.toUpperCase().indexOf("MAC") > -1) &&
       (browser.toUpperCase().indexOf("IE") > -1) &&
        wasInitialized() && resize > 0 )
  {
    // do something
  }
}

3. 2 Giải pháp

Tách biểu thức phức tạp thành những biểu thức con, giá trị của biểu thức được gán cho các biến mà tên biến mô tả ý nghĩa kết quả biểu thức.

void renderBanner() {
  final boolean isMacOs = platform.toUpperCase().indexOf("MAC") > -1;
  final boolean isIE = browser.toUpperCase().indexOf("IE") > -1;
  final boolean wasResized = resize > 0;

  if (isMacOs && isIE && wasInitialized() && wasResized) {
    // do something
  }
}

3. 3 Tại sao lại cần Refactor

Lý do chính để tách các biến là làm cho một biểu thức phức tạp trở nên dễ hiểu hơn, bằng cách chia nó thành các phần trung gian của nó.

Biểu thức phức tạp ở đây có thể là:

Điều kiện của toán tử if () hoặc một phần của toán tử ?: trong các ngôn ngữ phát triển dựa trên cú pháp của C. Một biểu thức số học dài mà không có kết quả trung gian. Tách một biến có thể là bước đầu tiên hướng tới việc thực hiện Extract Method nếu bạn thấy rằng biểu thức trích xuất được sử dụng ở những nơi khác trong code của bạn.

3. 4 Lợi ích

Code có thể đọc được nhiều hơn. Cách đặt tên biến cũng tương tự như cách đặt tên hàm, tên phương thức, hãy cố gắng đặt tên gợi nhớ đến chức năng, ý nghĩa của chúng.Ví dụ như customerTaxValue, cityUnemploymentRate, clientSalutationString, v.v.

3. 5 Nhược điểm

Nhiều biến hơn có trong code của bạn. Đổi lại, bạn có một chương trình dễ đọc hiểu code hơn.

3. 6 Làm thế nào để Refactor

  1. Chèn một dòng mới trước biểu thức có liên quan và khai báo một biến mới ở đó. Gán một phần của biểu thức phức tạp cho biến này.
  2. Thay thế một phần của biểu thức bằng biến mới.
  3. Lặp lại quá trình cho tất cả các phần phức tạp của biểu thức.

3.7 Ví dụ

Một ví dụ điển hình để ta có thể áp dụng phương pháp Extract Variable

double price() {
    // price is base price - quantity discount + shipping
    return quantity * itemPrice -
        Math.max(0, quantity - 500) * itemPrice * 0.05 +
        Math.min(quantity * itemPrice * 0.1, 100.0);
}

Áp dụng phương pháp Extract Variable, ngay lập tức ta có thể cho ra kết quả:

double price() {
   final double basePrice = quantity * itemPrice;
   final double quantityDiscount = Math.max(0, quantity - 500) * itemPrice * 0.05;
   final double shipping = Math.min(basePrice * 0.1, 100.0);
   return basePrice - quantityDiscount + shipping;
}

Thư giãn 1 chút, nếu áp dụng Extract Method cho phương thức này, ta sẽ thu được điều gì ? Và đây là câu trả lời:

double price() {
   return basePrice() - quantityDiscount() + shipping();
}
private double quantityDiscount() {
   return Math.max(0, quantity - 500) * itemPrice * 0.05;
}
private double shipping() {
   return Math.min(basePrice() * 0.1, 100.0);
}
private double basePrice() {
   return quantity * itemPrice;
}

Câu hỏi được đặt ra là : Vậy khi nào tôi sử dụng Extract Variable ? Câu trả lời là khi có nhiều biến địa phương, ta nên sử dụng Extract Variable. Nếu tôi đang ở trong một thuật toán với rất nhiều biến địa phương, tôi có thể không thể dễ dàng sử dụng Extract Method. Như trong ví dụ trên bạn có thể thấy, để kiểm soát được giá trị của các biến, nếu dùng Extract Method sẽ tăng thêm một số lượng lớn phương thức trong chương trình. Điều này đôi lúc làm cho chương trình rối rắm, khó kiểm soát vì có quá nhiều phương thức không cần thiết. Trong trường hợp này tôi sử dụng Extract Variable vẫn kiểm soát được giá trị của biến trong chương trình dù chỉ là tăng số lượng biến cục bộ trong một phương thức.

4. Inline Temp

4.1 Vấn đề

Bạn chỉ dùng một biến tạm thời để gán kết qủa cho một biểu thức đơn giản mà không làm gì thêm.

double hasDiscount(Order order) {
  double basePrice = order.basePrice();
  return (basePrice > 1000);
}

4.2 Giải pháp

Thay thế các tham chiếu đến biến bằng chính biểu thức đã được gán giá trị cho biến.

double hasDiscount(Order order) {
  return (order.basePrice() > 1000);
}

4.3 Tại sao lại cần Refactor

Các biến cục bộ (local variables) hầu như luôn được sử dụng như là một phần của Replace Temp with Query hoặc để mở đường cho Extract Method.

4. 4 Lợi ích

Kỹ thuật tái cấu trúc này hầu như không có lợi ích gì cho phương thức. Tuy nhiên, nếu biến được gán kết quả của một phương thức, bạn có thể cải thiện chút ít khả năng đọc và biên dịch của chương trình bằng cách loại bỏ các biến không cần thiết.

4. 5 Nhược điểm

Đôi khi các biến tạm thời được sử dụng để lưu trữ kết quả của một hoạt động "đắt tiền" được tái sử dụng nhiều lần (ví dụ như truy vấn database, khởi tạo đối tượng). Vì vậy, trước khi sử dụng kỹ thuật tái cấu trúc này, hãy đảm bảo rằng sự đơn giản sẽ không đánh đổi với hiệu suất và tốc độ của chương trình.

4. 6 Làm thế nào để Refactor

  1. Tìm tất cả các địa điểm sử dụng biến. Thay vì biến, sử dụng biểu thức đã được gán cho nó.
  2. Xóa khai báo của biến và nơi gán giá trị cho nó.

5. Replace Temp with Query

5. 1 Vấn đề

Bạn đặt kết quả của một biểu thức trong một biến địa phương để sử dụng sau này trong code của bạn.

double calculateTotal() {
  double basePrice = quantity * itemPrice;
  if (basePrice > 1000) {
    return basePrice * 0.95;
  }
  else {
    return basePrice * 0.98;
  }
}

5.2 Giải pháp

Di chuyển toàn bộ biểu thức sang một phương thức riêng biệt và trả về kết quả từ nó. Gọi phương thức thay vì sử dụng một biến. Có thể sử dụng phương thức mới được tạo ra này ở những nơi khác trong code của bạn nếu cần.

double calculateTotal() {
  if (basePrice() > 1000) {
    return basePrice() * 0.95;
  }
  else {
    return basePrice() * 0.98;
  }
}
double basePrice() {
  return quantity * itemPrice;
}

5. 3 Tại sao lại cần Refactor

Việc tái cấu trúc này có thể đặt nền tảng cho việc áp dụng Extract Method cho một phần của một phương thức rất dài (very long method).

Cùng một biểu thức đôi khi có thể được tìm thấy trong nhiều phương thức khác nhau. Đó là một lý do để xem xét tạo ra một phương thức được sử dụng phổ biến.

5.4 Lợi ích

Đọc mã. Mã trở nên dễ hiểu hơn. Ví dụ bạn có thể hiểu ngay về mục đích của phương thức getTax () so với dòng lệnh orderPrice () * 0.2. Nếu dòng được thay thế được sử dụng trong nhiều phương thức, code của bạn sẽ ngắn hơn.

5.5 Điều cần biết

Hiệu suất

Việc tái cấu trúc này có thể khiến cho bạn đặt ra câu hỏi: liệu phương pháp này có gây ảnh hưởng đến hiệu suất và tốc độ chạy của chương trình hay không ? Câu trả lời trung thực là: có, vì khi tính toán kết quả sẽ có thêm 1 gánh năng đó là gọi thêm một phương thức mới. Nhưng với các CPU tốc độ ngày nay và trình biên dịch xuất sắc, gánh nặng sẽ gần như luôn luôn là tối thiểu. Ngược lại, code có thể đọc được và khả năng sử dụng lại phương thức này ở những nơi khác trong chương trình của bạn- nhờ phương pháp tái cấu trúc này - là những lợi ích đáng chú ý.

Tuy nhiên, nếu biến temp của bạn được sử dụng để lưu trữ kết quả của một biểu thức thực sự tốn thời gian, bạn có thể muốn dừng lại việc sử dụng phương pháp tái cấu trúc này sau khi tách biểu thức sang một phương thức mới.

5. 6 Làm thế nào để Refactor

  1. Đảm bảo rằng giá trị được gán cho biến một lần và chỉ một lần trong phương pháp. Nếu không, sử dụng Split Temporary Variable để đảm bảo rằng biến sẽ được sử dụng chỉ để lưu trữ kết quả của biểu thức của bạn.
  2. Sử dụng Extract Method để đặt biểu thức quan tâm trong một phương thức mới. Hãy chắc chắn rằng phương pháp này chỉ trả về một giá trị và không thay đổi trạng thái của đối tượng. Nếu phương pháp ảnh hưởng đến trạng thái hiển thị của đối tượng, sử dụng Separate Query from Modifier.
  3. Thay thế biến bằng cuộc gọi đến phương thức mới của bạn. 5.7 Ví dụ Ta lấy một ví dụ đơn giản:
double getPrice() {
    int basePrice = quantity * itemPrice;
    double discountFactor;
    if (basePrice > 1000) discountFactor = 0.95;
    else discountFactor = 0.98;
    return basePrice * discountFactor;
}

Tương tự như 5.2, ta có thể ngay lập tức tách biểu thức tính "basePrice" sang một phương thức mới.

double getPrice() {
    final int basePrice = basePrice();
    final double discountFactor;
    if (basePrice > 1000) discountFactor = 0.95;
    else discountFactor = 0.98;
    
    return basePrice * discountFactor;
}
private int basePrice() {
    return quantity * itemPrice;
}

Nhưng với phần tính "discountFactor", tôi cũng muốn làm tương tự.

double getPrice() {
    return basePrice() * discountFactor();
}
private int basePrice() {
    return quantity * itemPrice;
}
private double discountFactor() {
    if (basePrice() > 1000) return 0.95;
    else return 0.98;
}

Tài liệu tham khảo

https://sourcemaking.com/refactoring Refactoring: Improving the Design of Existing Code by Martin Fowler


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í