Refactoring techniques - Composing Methods (part 2)
Bài đăng này đã không được cập nhật trong 3 năm
Mở đầu
Trong part1 của bài viết này, tôi đã giới thiệu với các bạn 5 phương pháp tái cấu trúc trong Composing Methods. Bài viết lần này, tôi xin giới thiệu những phương pháp tái cấu trúc còn lại trong Composing Methods.
1. Split Temporary Variable
1. 1 Vấn đề
Bạn có một biến địa phương được sử dụng để lưu trữ các giá trị trung gian khác nhau bên trong một phương thức (ngoại trừ các biến được sử dụng trong các vòng lặp).
double temp = 2 * (height + width);
System.out.println(temp);
temp = height * width;
System.out.println(temp);
1.2 Giải pháp
Sử dụng các biến khác nhau cho các giá trị khác nhau. Mỗi biến chỉ nên chịu trách nhiệm về một điều duy nhất.
final double perimeter = 2 * (height + width);
System.out.println(perimeter);
final double area = height * width;
System.out.println(area);
1.3 Tại sao lại cần Refactor
Nếu bạn đang hạn chế số lượng các biến bên trong một chức năng và sử dụng lại các biến trung gian cho các mục đích không liên quan nhau, bạn chắc chắn sẽ gặp phải vấn đề ngay khi bạn cần thay đổi đoạn code có chứa các biến trung gian đó. Bạn sẽ phải kiểm tra lại từng trường hợp sử dụng biến để đảm bảo rằng các giá trị chính xác sẽ được sử dụng. Ví dụ: bạn sử dụng 1 biến temp nhiều nơi, 1 lúc nào đó bạn sẽ đặt ra câu hỏi : không biết tại vị trí này giá trị của temp bằng bao nhiêu ? qua vòng lặp này thì temp nhận giá trị nào....
1. 4 Lợi ích
Mỗi thành phần của chương trình phải chịu trách nhiệm về chỉ một và một điều duy nhất. Điều này làm cho nó dễ dàng hơn nhiều cho công tác bảo trì sau này, vì bạn có thể dễ dàng thay thế bất kỳ điều đặc biệt mà không sợ hiệu ứng không mong muốn. Code trở nên dễ đọc hơn. Nếu một biến đã được tạo ra từ lâu rồi, có lẽ nó có một cái tên không giải thích bất cứ điều gì: k, a2, value ... Nhưng bạn có thể khắc phục tình huống này bằng cách đặt tên các biến mới theo cách dễ hiểu. Tên như vậy có thể giống với customerTaxValue, cityUnemploymentRate, clientSalutationString.... Kỹ thuật tái cấu trúc này hữu ích nếu bạn sử dụng Extract Method sau đó. (Mỗi đoạn code tách ra thành 1 phương thức riêng biệt đảm nhiệm 1 chức năng riêng biệt)
1.5 Làm thế nào để tái cấu trúc
- Tìm vị trí đầu tiên trong code mà biến được gán giá trị. Ở đây bạn nên đổi tên biến với tên tương ứng với giá trị được gán.
- Sử dụng tên mới thay vì tên cũ trong những nơi có giá trị của biến được sử dụng.
- Lặp lại nếu cần cho những nơi mà biến được gán một giá trị khác.
2. Remove Assignments to Parameters
2.1 Vấn đề
Một số tham số đầu vào của phương thức bị thay đổi giá trị trong thân phương thức.
int discount(int inputVal, int quantity) {
if (inputVal > 50) {
inputVal -= 2;
}
//...
}
2.2 Giải pháp
Sử dụng một biến địa phương thay vì một tham số đầu vào.
int discount(int inputVal, int quantity) {
int result = inputVal;
if (inputVal > 50) {
result -= 2;
}
//...
}
2.3 Tại sao lại cần Refactor
Đầu tiên, nếu một tham số được truyền qua tham chiếu, sau đó khi giá trị tham số được thay đổi bên trong phương thức, giá trị này được truyền cho đối số yêu cầu gọi phương thức này. Rất thường xuyên, điều này xảy ra vô tình và dẫn đến những hậu quả không mong muốn.
Thứ hai, gán quá nhiều giá trị khác nhau cho một tham số đơn giản làm cho bạn khó khăn để biết những gì dữ liệu nên được chứa trong tham số tại bất kỳ điểm cụ thể trong thời gian. Vấn đề trở nên tồi tệ hơn nếu tham số của bạn và các nội dung của nó được ghi lại nhưng giá trị thực tế có thể khác với những gì được mong đợi bên trong phương thức.
2.4 Lợi ích
Mỗi yếu tố của chương trình chỉ nên chịu trách nhiệm về một điều duy nhất. Điều này làm cho việc bảo trì mã trở nên dễ dàng hơn rất nhiều, vì bạn có thể thay thế mã một cách an toàn mà không có bất kỳ phản ứng phụ nào. Việc tái cấu trúc này mở đường cho việc sử dụng phương pháp Extract Method ngay sau đó.(Tách các đoạn code thành các phương thức riêng biệt, sau này các phương thức này có thể được gọi lại trong những đoạn code khác nhau của chương trình)
2.5 Làm thế nào để tái cấu trúc
- Tạo biến cục bộ và gán giá trị của tham số đầu vào cho biến cục bộ đó.
- Trong tất cả các dòng code sử dụng tham số đầu vào, hãy thay thế tham số bằng biến cục bộ mới của bạn.
2.6 Ví dụ
Tôi sẽ lấy 1 ví dụ đơn giản để có thể sử dụng phương pháp Remove Assignments to Parameters.
int discount (int inputVal, int quantity, int yearToDate) {
if (inputVal > 50) inputVal -= 2;
if (quantity > 100) inputVal -= 1;
if (yearToDate > 10000) inputVal -= 4;
return inputVal;
}
Áp dụng phương pháp này, ta được kết qủa:
int discount (int inputVal, int quantity, int yearToDate) {
int result = inputVal;
if (inputVal > 50) result -= 2;
if (quantity > 100) result -= 1;
if (yearToDate > 10000) result -= 4;
return result;
}
Bạn cũng có thể sử dụng từ khóa "final" cho các tham số đầu vào.
int discount (final int inputVal, final int quantity, final int yearToDate) {
int result = inputVal;
if (inputVal > 50) result -= 2;
if (quantity > 100) result -= 1;
if (yearToDate > 10000) result -= 4;
return result;
}
Tại sao lại "có thể " sử dụng từ khóa "final" ? Tôi thừa nhận rằng tôi không sử dụng từ khóa "final" nhiều, bởi vì trong các phương thức ngắn, final thực sự không có tác dụng nhiều. Tôi sử dụng nó trong một phương thức dài để giúp tôi kiểm soát xem có bất cứ điều gì đang thay đổi các tham số hay không.
3. Replace Method with Method Object
3.1 Vấn đề
Bạn có một phương pháp dài, trong đó các biến địa phương nằm xen kẽ nhau mà bạn không thể áp dụng [Extract Method].
class Order {
//...
public double price() {
double primaryBasePrice;
double secondaryBasePrice;
double tertiaryBasePrice;
// long computation.
//...
}
}
3. 2 Giải pháp
Chuyển đổi phương thức thành một lớp riêng biệt để các biến địa phương trở thành các trường của lớp. Sau đó, bạn có thể chia phương thức thành một số phương thức trong cùng một lớp.
class Order {
//...
public double price() {
return new PriceCalculator(this).compute();
}
}
class PriceCalculator {
private double primaryBasePrice;
private double secondaryBasePrice;
private double tertiaryBasePrice;
public PriceCalculator(Order order) {
// copy relevant information from order object.
//...
}
public double compute() {
// long computation.
//...
}
}
3. 3 Tại sao lại cần Refactor
Một phương thức quá dài và bạn không thể tách nó ra do có quá nhiều biến địa phương được sử dụng. Bước đầu tiên là tách toàn bộ phương thức vào một lớp riêng biệt và biến các biến cục bộ của nó thành các trường của lớp. Thứ nhất, điều này cho phép quản lý các vấn đề ở cấp lớp. Thứ hai, nó mở đường cho việc tách một phương thức lớn và khó sử dụng thành những cái nhỏ và loại bỏ những thứ không cần thiết trong lớp gốc.
3.4 Lợi ích
Chuyển đổi từ một phương thức dài thành một lớp cho phép tách phương thức thành các phương thức con trong lớp, mà không làm hỏng lớp gốc bằng các phương thức thực sự hữu ích. Sau này, thay vì thêm code vào phương thức cũ, chúng ta có thể viết thêm phương thức mới vào lớp mới.
3.5 Nhược điểm
Một lớp khác được thêm vào, làm tăng sự phức tạp tổng thể của chương trình.
3.6 Làm thế nào để tái cấu trúc
- Tạo một lớp mới. Tên nó dựa trên mục đích của phương thức mà bạn đang tái cấu trúc.
- Tạo một trường private riêng biệt cho mỗi biến địa phương của phương thức.
- Tạo một constructor nhận các tham số đầu vào là các giá trị của tất cả các biến địa phương trong phương thức.
- Khai báo phương thức chính (main method) và sao chép mã của phương thức gốc vào nó, thay thế các biến cục bộ bằng các trường private.
- Thay thế phần thân của phương thức ban đầu trong lớp gốc bằng cách tạo đối tượng phương thức (method object)và gọi phương thức chính của nó.
3.7 Ví dụ
Một ví dụ thích hợp của phường pháp tái cấu trúc này đòi hỏi một lớp chứa phương thức dài.
Class Account...
int gamma (int inputVal, int quantity, int yearToDate) {
int importantValue1 = (inputVal * quantity) + delta();
int importantValue2 = (inputVal * yearToDate) + 100;
if ((yearToDate - importantValue1) > 100)
importantValue2 -= 20;
int importantValue3 = importantValue2 * 7;
// and so on.
return importantValue3 - 2 * importantValue1;
}
Chúng ta bắt đầu tạo một lớp mới, tạo một trường của lớp gốc với khai báo "final" và một trường cho mỗi tham số và biến tạm thời trong phương thức.
class Gamma...
private final Account account;
private int inputVal;
private int quantity;
private int yearToDate;
private int importantValue1;
private int importantValue2;
private int importantValue3;
Hãy thêm một constructor cho lớp Gamma mới này :
public Gamma (Account source, int inputValArg, int quantityArg, int yearToDateArg) {
this.account = source;
this.inputVal = inputValArg;
this. quantity = quantityArg;
this.yearToDate = yearToDateArg;
}
Giờ thì chúng ta hãy di chuyển phương thức dài dòng của lớp gốc sang lớp mới ...
int compute () {
importantValue1 = (inputVal * quantity) + account.delta();
importantValue2 = (inputVal * yearToDate) + 100;
if ((yearToDate - importantValue1) > 100)
importantValue2 -= 20;
int importantValue3 = importantValue2 * 7;
// and so on.
return importantValue3 - 2 * importantValue1;
}
...và trong lớp gốc, chỉ cần thế này là đủ:
int gamma (int inputVal, int quantity, int yearToDate) {
return new Gamma(this, inputVal, quantity, yearToDate).compute();
}
Sẽ có bạn đặt ra câu hỏi : Làm vậy để làm gì ? Câu trả lời là: ngay bây giờ, chúng ta đã có thể áp dụng được phương pháp Extract Method vào phương thức compute() mà không phải lo lắng về việc truyền tham số đầu vào.
int compute () {
importantValue1 = (inputVal * quantity) + account.delta();
importantValue2 = (inputVal * yearToDate) + 100;
importantThing();
int importantValue3 = importantValue2 * 7;
// and so on.
return importantValue3 - 2 * importantValue1;
}
void importantThing() {
if ((yearToDate - importantValue1) > 100)
importantValue2 -= 20;
}
4. Substitute Algorithm
4.1 Vấn đề
Bạn muốn thay thế một thuật toán hiện có bằng một thuật toán mới?
String foundPerson(String[] people){
for (int i = 0; i < people.length; i++) {
if (people[i].equals("Don")){
return "Don";
}
if (people[i].equals("John")){
return "John";
}
if (people[i].equals("Kent")){
return "Kent";
}
}
return "";
}
4.2 Giải pháp
Thay thế phần thân của phương thức thực hiện thuật toán bằng một thuật toán mới.
String foundPerson(String[] people){
List candidates =
Arrays.asList(new String[] {"Don", "John", "Kent"});
for (int i=0; i < people.length; i++) {
if (candidates.contains(people[i])) {
return people[i];
}
}
return "";
}
4.3 Tại sao lại cần Refactor
- Tái cấu trúc lần lượt từng phần trong chương trình không phải là phương pháp duy nhất để cải tiến một chương trình. Đôi khi một phương pháp như vậy là lộn xộn, thậm chí là phá hủy chương trình vì chương trình có quá nhiều module nhỏ. Và có lẽ bạn đã tìm ra một thuật toán đơn giản hơn và hiệu quả hơn nhiều. Nếu đúng như vậy, bạn chỉ cần thay thế thuật toán cũ bằng thuật toán mới.
- Theo thời gian, thuật toán của bạn có thể được kết hợp vào một thư viện hoặc framework nổi tiếng và bạn muốn thoát khỏi việc thực hiện độc lập của mình, để đơn giản hóa việc bảo trì.
- Các yêu cầu cho chương trình của bạn có thể thay đổi quá nhiều đến mức thuật toán hiện tại của bạn không thể đáp ứng được yêu cầu.
4.4 Làm thế nào để tái cấu trúc
- Hãy chắc chắn rằng bạn đã đơn giản hóa các thuật toán hiện có càng nhiều càng tốt. Di chuyển mã không quan trọng sang các phương pháp khác bằng cách sử dụng Extract Method.
- Tạo thuật toán mới của bạn theo một phương pháp mới. Thay thế thuật toán cũ bằng thuật toán mới và bắt đầu thử nghiệm chương trình.
- Nếu kết quả không khớp, hãy quay lại thực hiện cũ và so sánh kết quả. Xác định nguyên nhân của sự khác biệt. Trong khi nguyên nhân thường là một lỗi trong thuật toán cũ, nó có thể do một cái gì đó không làm việc trong thuật toán mới.
- Khi tất cả các bài kiểm tra được hoàn thành thành công, xóa các thuật toán cũ cho tốt!
Tài liệu tham khảo
https://sourcemaking.com/refactoring Refactoring: Improving the Design of Existing Code by Martin Fowler
All rights reserved