Bài 12 · Nâng cao · 32 phút
Dự án: refactor code “bẩn”
Biên soạn bởi Nguyễn Anh Tuấn
Dự án cuối khoá: 3 thử thách refactor bằng TypeScript - hàm rối, God object, code smell - biến "bẩn" thành sạch từng bước nhờ characterization test.
Đây là lúc biến lý thuyết thành kỹ năng thực hành. Ba thử thách dưới đây cho mèo con một đoạn TypeScript "bẩn" thật - không phải ví dụ đơn giản hoá - và yêu cầu refactor từng bước. Mỗi thử thách có khối "Hoàn thành khi" để tự chấm.
Nguyên tắc cốt lõi xuyên suốt cả ba thử thách: refactor = giữ nguyên hành vi, đổi cấu trúc. Không có test đỡ lưng thì không biết mình có lỡ đổi hành vi không. Vì vậy, mỗi thử thách bắt đầu bằng viết characterization test cho code gốc trước khi đụng vào.
- ▸Làm trên TypeScript (hoặc JavaScript thuần nếu bạn chưa quen TS - tư duy giống nhau).
- ▸Viết characterization test TRƯỚC: chốt "code hiện đang làm gì" để refactor mạnh dạn.
- ▸Refactor TỪNG BƯỚC nhỏ, chạy test sau mỗi bước - không sửa ào một lần rồi mới test.
Hàm dưới đây tính chiết khấu và tổng tiền cho đơn hàng. Nó chạy được, nhưng vi phạm nhiều thứ đã học ở bài Hàm nhỏ và bài Đặt tên: hàm dài làm nhiều việc, lồng sâu, magic number rải khắp, tên biến viết tắt.
order.ts - truoc refactor
function calc(o: { items: { p: number; q: number }[]; vip: boolean }): number {
let t = 0;
for (let i = 0; i < o.items.length; i++) {
t += o.items[i].p * o.items[i].q;
}
let d = 0;
if (o.vip) {
if (t > 1000000) {
d = t * 0.15;
} else if (t > 500000) {
d = t * 0.1;
} else {
d = t * 0.05;
}
} else {
if (t > 1000000) {
d = t * 0.08;
} else if (t > 500000) {
d = t * 0.05;
}
}
return t - d;
} Yêu cầu:
- Viết ít nhất một characterization test - gọi calc() với vài đầu vào, ghi lại kết quả thực tế làm kỳ vọng.
- Dùng extract function tách thành ít nhất: một hàm tính tổng tiền hàng, một hàm tính chiết khấu - đặt tên rõ ý định.
- Thay toàn bộ magic number (0.15, 1000000...) bằng hằng số có tên giải thích ý nghĩa.
- Chạy characterization test sau khi xong - phải xanh.
Hoàn thành khi
Lớp dưới đây ôm đồm cả dữ liệu người dùng lẫn nghiệp vụ gửi thông báo và tạo báo cáo - điển hình của God object vi phạm SRP. Cũng là một hybrid xấu: vừa có public field như data structure, vừa có method nghiệp vụ như object - chủ đề từ bài Object vs Data Structure.
user-manager.ts - truoc refactor
class UserManager {
// public field lan lon voi method nghiep vu
public name: string;
public email: string;
public role: string;
public lastLogin: Date;
public loginCount: number;
public notificationsSent: string[] = [];
constructor(name: string, email: string, role: string) {
this.name = name;
this.email = email;
this.role = role;
this.lastLogin = new Date();
this.loginCount = 0;
}
// nghiep vu 1: kiem tra quyen truy cap
canAccess(resource: string): boolean {
if (this.role === 'admin') return true;
if (resource === 'dashboard' && this.role === 'manager') return true;
return false;
}
// nghiep vu 2: ghi nhat ky dang nhap
recordLogin(): void {
this.lastLogin = new Date();
this.loginCount++;
console.log(`User ${this.name} logged in at ${this.lastLogin}`);
}
// nghiep vu 3: gui thong bao - giao tiep he thong ngoai
sendNotification(message: string): void {
// gia lap gui email
console.log(`Sending email to ${this.email}: ${message}`);
this.notificationsSent.push(message);
}
// nghiep vu 4: tao bao cao - dung dinh dang rieng
generateReport(): string {
return `=== BAO CAO NGUOI DUNG ===\nTen: ${this.name}\nEmail: ${this.email}\nVai tro: ${this.role}\nLan dang nhap cuoi: ${this.lastLogin.toISOString()}\nTong so lan: ${this.loginCount}\nThong bao da gui: ${this.notificationsSent.length}`;
}
} Yêu cầu:
- Tách dữ liệu thuần thành một interface hoặc type riêng (UserData hay tương tự) - không có method.
- Tách các trách nhiệm thành ít nhất 2 lớp hoặc module rõ ràng: ví dụ lớp quản lý quyền truy cập, lớp ghi nhật ký hoạt động, và một hàm/module tạo báo cáo.
- Mỗi lớp/module mới chỉ có một lý do để thay đổi - đặt tên phản ánh đúng trách nhiệm đó.
Hoàn thành khi
Module dưới đây dùng thật trong một hệ thống xử lý đơn hàng giả lập. Nó chứa nhiều code smell đã học ở bài Code smell & refactoring: logic trùng lặp, dead code, catch rỗng nuốt lỗi, và trả null - vi phạm bài Xử lý lỗi sạch.
order-service.ts - truoc refactor
interface Order { id: string; items: { name: string; price: number }[]; status: string; }
// ham tra null khi khong tim thay
function findOrder(orders: Order[], id: string): Order | null {
for (let i = 0; i < orders.length; i++) {
if (orders[i].id === id) return orders[i];
}
return null;
}
// logic tinh tong bi sao chep y het o hai noi
function getOrderTotal(order: Order): number {
let total = 0;
for (const item of order.items) {
total += item.price;
}
return total;
}
function printOrderSummary(order: Order): void {
// sao chep y het getOrderTotal thay vi goi lai
let total = 0;
for (const item of order.items) {
total += item.price;
}
console.log(`Don hang ${order.id}: ${total} VND`);
}
// catch rong - nuot loi im lang
function processOrder(orders: Order[], id: string): void {
try {
const order = findOrder(orders, id);
if (order === null) return; // caller khong biet co loi
order.status = 'processing';
console.log(`Dang xu ly don ${order.id}`);
} catch (e) {
// TODO: xu ly sau
}
}
// dead code - ham nay khong con duoc goi o dau
function legacyExportOrder(order: Order): string {
return JSON.stringify(order);
}
// tra null thay vi mang rong
function getOrdersByStatus(orders: Order[], status: string): Order[] | null {
const result = orders.filter((o) => o.status === status);
if (result.length === 0) return null;
return result;
} Yêu cầu - làm theo thứ tự:
- Bước 0 (bắt buộc): viết characterization test cho ít nhất processOrder và getOrdersByStatus trước khi đụng vào code.
- Bước 1: xoá trùng lặp - printOrderSummary gọi lại getOrderTotal thay vì sao chép.
- Bước 2: xoá dead code (legacyExportOrder). Chạy test.
- Bước 3: sửa getOrdersByStatus trả mảng rỗng [] thay vì null.
- Bước 4: sửa catch rỗng trong processOrder - ít nhất phải log + rethrow hoặc fail rõ ràng khi không tìm thấy đơn.
- Chạy characterization test sau mỗi bước.
Hoàn thành khi
Bạn đã đi qua 11 bài, từ nền tảng đến tổng hợp:
- Vì sao code sạch - code đọc nhiều hơn viết; quy tắc Boy Scout.
- Đặt tên - tên rõ ý định, searchable, không gây nhiễu.
- Hàm nhỏ - một việc, một mức trừu tượng, không flag argument.
- Bình luận - giải thích "vì sao", không giải thích "cái gì".
- Định dạng - đọc như bài báo; để Prettier lo chi tiết.
- Object vs Data Structure - bất đối xứng, Luật Demeter, DTO.
- Xử lý lỗi sạch - exception, không trả null, không nuốt lỗi.
- Ranh giới (Boundaries) - bọc API ngoài sau interface của mình.
- Unit test sạch - F.I.R.S.T, AAA, tên test kể chuyện.
- Code smell & refactoring - nhận diện mùi, refactor an toàn có test.
- Thiết kế đơn giản - bốn quy tắc Kent Beck; YAGNI; cầu nối sang SOLID.
Quan trọng hơn danh sách kỹ thuật là thói quen: "code được đọc nhiều hơn viết" - mỗi lần viết là viết thư gửi cho người đọc tương lai (thường là chính mèo con sau vài tháng). Và quy tắc Boy Scout: để mỗi chỗ mình chạm vào sạch hơn một chút - không cần đại tu, chỉ cần đều đặn. Hai thói quen đó, bền hơn mọi kỹ thuật.
Bước tiếp theo trong lộ trình
Câu hỏi thường gặp
Được. Các thử thách viết theo kiểu TypeScript đọc như JavaScript với chú thích kiểu - nếu bỏ qua phần : string, : number, interface thì vẫn là JavaScript chạy được. Mục tiêu là luyện tư duy refactor, không phải kiểm tra TypeScript.
Refactor nghĩa là "đổi cấu trúc, giữ hành vi". Không có test thì bạn không biết mình có lỡ thay đổi hành vi không. Characterization test không phán xét code đúng/sai nghiệp vụ - nó chỉ chốt "code hiện đang làm gì" để bạn refactor mạnh dạn và phát hiện lệch ngay.
Dừng khi đạt tiêu chí "Hoàn thành khi" trong từng thử thách. Sạch không phải tuyệt đối - là đạt ngưỡng: đọc được, mỗi phần một việc, không trùng lặp rõ ràng, xử lý lỗi không nuốt im. Vượt quá đó mà refactor thêm là bắt đầu tốn thời gian vào trừu tượng hoá chưa cần.
Lớp bình thường có nhiều method vẫn xoay quanh một trách nhiệm (vd một class User với method getAge, formatName). God object ôm nhiều trách nhiệm KHÁC NHAU: vừa tính toán, vừa giao tiếp DB, vừa gửi mail, vừa sinh báo cáo - nhiều lý do để thay đổi. Dấu hiệu nhận diện: "nếu thay đổi nghiệp vụ X, lớp này có phải sửa không?" - God object trả lời "có" với rất nhiều X không liên quan.
Tick những điều em tự tin làm được. Càng lên cao, em càng hiểu sâu.
Trả lời vài câu để chắc rằng em đã nắm bài.
Vì sao phải viết characterization test TRƯỚC khi refactor, không phải sau?
- 1
Thử thách 1: tách hàm rối
Làm trọn Thử thách 1 trong bài: viết ít nhất một characterization test cho hàm gốc, sau đó refactor thành các hàm nhỏ với tên rõ ý định và hằng số thay magic number. Chạy test trước và sau để xác nhận hành vi không đổi.
Hoàn thành khi: Hàm ban đầu được tách thành ≥2 hàm nhỏ có tên rõ nghĩa; magic number thay bằng hằng số có tên; có ít nhất một characterization test chạy xanh trước và sau refactor.
- 2
Thử thách 2: tách God object
Làm trọn Thử thách 2: tách lớp ôm đồm thành ít nhất hai lớp - một lớp DTO (data structure thuần) và một lớp có hành vi rõ trách nhiệm. Đặt tên lớp và method phản ánh đúng trách nhiệm mới.
Hoàn thành khi: Lớp gốc được tách thành ≥2 lớp; không còn public field lẫn lộn với method nghiệp vụ trong cùng lớp; DTO chỉ giữ dữ liệu, lớp hành vi không có field thừa.
- 3
Thử thách 3: xử lý lỗi và trùng lặp
Làm trọn Thử thách 3: viết characterization test TRƯỚC, sau đó refactor TỪNG BƯỚC - xoá trùng lặp, sửa catch rỗng (nuốt lỗi), thay null trả về bằng mảng rỗng hoặc kiểu thích hợp. Chạy test sau MỖI bước.
Hoàn thành khi: Characterization test viết trước refactor và vẫn xanh sau khi xong; không còn catch rỗng; không còn trả null cho danh sách; không còn đoạn logic sao chép y hệt nhau.
- 4
Nhìn lại khoá học
Sau 12 bài, viết 3-5 câu tổng kết: (1) nguyên tắc nào bạn thấy hữu ích nhất? (2) mùi code bạn hay gặp nhất trong dự án của mình là gì? (3) một việc cụ thể bạn sẽ làm khác đi trong lần viết code tiếp theo.
Hoàn thành khi: Ba câu trả lời cụ thể - không chung chung; ít nhất một câu nêu được tên mùi code (vd "hàm dài", "magic number", "catch rỗng") và cách sửa.
- 5
Boy Scout trên dự án thật
Chọn một file trong dự án của mèo con. Áp quy tắc Boy Scout: thực hiện MỘT cải thiện nhỏ (đổi tên mơ hồ, tách một hàm rối, xoá dead code, sửa catch rỗng) mà không đổi hành vi. Commit riêng cải thiện đó.
Hoàn thành khi: Một commit rõ ràng chỉ cải thiện chất lượng đọc (không đổi hành vi); nêu được mùi code nào vừa được xử lý và nguyên tắc nào áp dụng.
- 6
Chia sẻ điều học được
Viết một ghi chú ngắn hoặc bài đăng về điều bạn học được từ khoá này (trên mạng xã hội, blog cá nhân, hay group học lập trình). Kèm hashtag #meohamhoc.
Hoàn thành khi: Một chia sẻ trung thực nêu ít nhất một nguyên tắc cụ thể và cách bạn đã áp dụng (hoặc kế hoạch áp dụng) - không phải chỉ "khoá hay lắm".