Refactor an toàn
Nội dung bài · 5 mục
- 1.Khái niệm
- 2.Ví dụ
- 3.Thử ngay
- 4.Lỗi hay gặp
- 5.Tóm tắt
Hai bài trước chỉ ra chỗ nên sửa. Nhưng sửa code đang chạy rất dễ gây lỗi: gõ
nhầm >= thành > là đơn đúng 500.000 bị tính phí giao hàng. Bài này sửa từng
bước nhỏ, sau mỗi bước chạy test để chắc hành vi không đổi.
Khái niệm
🪛 Refactor: thay đổi cấu trúc bên trong của code để dễ đọc, dễ sửa hơn mà không làm thay đổi hành vi bên ngoài.
Quy trình một bước refactor:
| Bước | Làm gì |
|---|---|
| 1 | Chạy test, đảm bảo tất cả đang xanh |
| 2 | Sửa một chỗ nhỏ: đổi tên, đặt hằng, tách method |
| 3 | Chạy lại test. Đỏ thì hoàn tác bằng git restore . |
| 4 | Xanh thì commit, rồi quay lại bước 2 |
git restore . bỏ mọi thay đổi chưa commit, đưa các file về đúng commit
gần nhất. Chưa có test thì viết test trước, như chương Test, rồi mới
refactor.
Ví dụ
Đưa shippingFee của bài Đặt tên và method ngắn vào class
ShippingCalculator trong src/main/java/com/shop/api/ của shop-api.
Test trong src/test/java/com/shop/api/ chốt hành vi hiện tại, gồm cả mốc
500.000:
// ShippingCalculator.java
package com.shop.api;
class ShippingCalculator {
public long fee(long orderTotal,
boolean isMember, String city) {
if (orderTotal >= 500000) {
return 0;
}
if (isMember) {
return 0;
}
if (city.equals("Hà Nội")) {
return 20000;
}
return 35000;
}
}
// ShippingCalculatorTests.java
package com.shop.api;
import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.params.*;
import org.junit.jupiter.params.provider.*;
class ShippingCalculatorTests {
@ParameterizedTest
@CsvSource({
"500000, false, Hà Nội, 0",
"200000, true, Đà Nẵng, 0",
"200000, false, Hà Nội, 20000",
"200000, false, Đà Nẵng, 35000"
})
public void feeReturnsExpected(
long orderTotal, boolean isMember,
String city, long expected) {
var calculator = new ShippingCalculator();
long fee =
calculator.fee(orderTotal, isMember, city);
assertEquals(expected, fee);
}
}Bước refactor đầu tiên: số trần 500000 thành hằng có tên, như bài Code
smell. Hằng chỉ class này dùng nên khai báo private static final:
// ShippingCalculator.java
package com.shop.api;
class ShippingCalculator {
private static final long FREE_SHIPPING_THRESHOLD =
500000;
public long fee(long orderTotal,
boolean isMember, String city) {
if (orderTotal >= FREE_SHIPPING_THRESHOLD) {
return 0;
}
if (isMember) {
return 0;
}
if (city.equals("Hà Nội")) {
return 20000;
}
return 35000;
}
}- Chạy
./mvnw test: vẫn xanh, nên bước này không đổi hành vi. Commit. - Mỗi bước tiếp theo, như đặt hằng cho 20000 và 35000, cũng đi đủ vòng: sửa, test, commit.
Thử ngay
Giả sử lúc đặt hằng, bạn gõ nhầm >= thành > ở if đầu tiên:
// ShippingCalculator.java
package com.shop.api;
class ShippingCalculator {
private static final long FREE_SHIPPING_THRESHOLD =
500000;
public long fee(long orderTotal,
boolean isMember, String city) {
if (orderTotal > FREE_SHIPPING_THRESHOLD) {
return 0;
}
if (isMember) {
return 0;
}
if (city.equals("Hà Nội")) {
return 20000;
}
return 35000;
}
}Chạy ./mvnw test.
Đoán trước khi chạy: test có bắt được không? Dòng dữ liệu nào đỏ?
Xem kết quả
[ERROR] com.shop.api.ShippingCalculatorTests.feeReturnsExpected(long, boolean, String, long)[1] -- Time elapsed: 0.163 s <<< FAILURE!
...
[ERROR] Failures:
[ERROR] ShippingCalculatorTests.feeReturnsExpected:24 expected: <0> but was: <20000>
[ERROR] Tests run: 5, Failures: 1, Errors: 0, Skipped: 0
[INFO] BUILD FAILUREBắt được, nhờ dòng dữ liệu ở đúng mốc 500.000: [1] là dòng đầu tiên
trong @CsvSource. Ba dòng còn lại vẫn xanh, nên nếu thiếu dòng này thì lỗi
lọt qua. Sửa lại >= thì cả bốn xanh. Tổng số test trên máy bạn còn tuỳ
vào các test đã có trong project.
Lỗi hay gặp
Vừa refactor vừa thêm tính năng. Test đỏ thì không biết do refactor làm hỏng hay do tính năng mới, và commit trộn hai việc rất khó review.
# SAI — một commit vừa dọn code vừa thêm phí hoả tốc
git commit -am "Refactor phí giao hàng + hoả tốc"# ĐÚNG — commit refactor trước, tính năng sau
git commit -am "Đặt hằng FREE_SHIPPING_THRESHOLD"
git commit -am "Thêm phí giao hoả tốc"Tóm tắt
- Refactor đổi cấu trúc, giữ nguyên hành vi.
- Có test xanh trước, sửa một bước nhỏ, chạy test, commit.
- Test phải có dữ liệu ở mốc ranh giới thì mới bắt được lỗi
>=và>. - Không trộn refactor với thêm tính năng trong cùng một commit.
Tự kiểm tra
0/3 câuĐang refactor, chạy test thấy đỏ. Nên làm gì trước tiên?
Việc nào KHÔNG phải refactor?
Code chưa có test mà cần refactor. Nên làm gì trước?