Bài 8 · Nâng cao · 30 phút· Cập nhật 11/06/2026
Dự án: refactor codebase legacy theo nguyên tắc SOLID
Biên soạn bởi Nguyễn Anh Tuấn
Ví dụ thực tế đặt bạn vào tình huống nhận nhiệm vụ refactor codebase legacy theo nguyên tắc SOLID để cải thiện khả năng bảo trì, mở rộng và kiểm thử.
Đề bài: dịch vụ ghi danh khoá học. Bản đầu tiên chạy được, nhưng vi phạm gần như mọi nguyên tắc SOLID:
❌ Trước - God class, gắn chặt mọi thứ
class EnrollmentService {
enroll(userId: string, courseId: string, card: Card): void {
// (1) kiem tra hop le
if (!userId || !courseId) throw new Error("Thieu thong tin");
// (2) lay gia + thanh toan - gan chat Stripe & DB cu the
const price = mysql.query("SELECT price FROM courses WHERE id=?", courseId);
stripe.charge(card, price);
// (3) luu ghi danh - gan chat MySQL
mysql.query("INSERT INTO enrollments(user, course) VALUES(?,?)", userId, courseId);
// (4) gui email - gan chat SMTP
smtp.send(userId, "Ban da ghi danh thanh cong!");
}
} - ▸SRP: một lớp gánh 4 việc - kiểm tra, thanh toán, lưu trữ, gửi mail.
- ▸DIP: phụ thuộc thẳng vào mysql/stripe/smtp cụ thể (global) → coupling chặt.
- ▸OCP: thêm cổng thanh toán (MoMo) hay đổi DB là phải mổ lại lớp này.
- ▸Testability ≈ 0: gọi enroll() là charge thẻ thật, ghi DB thật, gửi mail thật.
Trước khi sửa, hãy chốt hành vi hiện tại bằng một characterization test để refactor không vô tình đổi hành vi:
Đặc tả hành vi hiện tại (lưới an toàn)
it("ghi danh: charge dung gia roi luu + bao tin", () => {
// thay the cac dich vu that bang spy de QUAN SAT hanh vi hien tai
const calls = trackCalls(); // ghi lai charge/insert/send da xay ra
new EnrollmentService().enroll("u1", "c1", card);
expect(calls).toEqual(["charge", "insert", "send"]); // chot thu tu hien tai
}); Vì sao test trước?
Chẻ God class thành các lớp nhỏ, mỗi lớp một việc:
Tách theo SRP
class EnrollmentValidator {
validate(userId: string, courseId: string): void {
if (!userId || !courseId) throw new Error("Thieu thong tin");
}
}
class PaymentService { pay(card: Card, amount: number): void { /* ... */ } }
class EnrollmentRepo { save(userId: string, courseId: string): void { /* ... */ } }
class EnrollmentMailer { confirm(userId: string): void { /* ... */ } } - ▸Mỗi lớp giờ có một lý do để thay đổi duy nhất.
- ▸Đã test được từng phần riêng (vd PaymentService) - dù vẫn còn gắn chi tiết.
- ▸Đây là bước mở đường: tách xong, đưa interface + tiêm phụ thuộc rất tự nhiên.
Đưa interface vào các ranh giới hay đổi, rồi tiêm implementation từ ngoài:
Sau - nghiệp vụ chỉ phụ thuộc abstraction
interface PaymentGateway { charge(card: Card, amount: number): void; } // OCP: them cong = them lop
interface EnrollmentRepository { save(userId: string, courseId: string): void; } // DIP
interface Mailer { confirm(userId: string): void; } // ISP: interface nho
interface PriceLookup { of(courseId: string): number; } // tra cuu gia
class EnrollmentService {
constructor(
private validator: EnrollmentValidator,
private payments: PaymentGateway, // tiem abstraction
private repo: EnrollmentRepository,
private mailer: Mailer,
private prices: PriceLookup,
) {}
enroll(userId: string, courseId: string, card: Card): void {
this.validator.validate(userId, courseId);
this.payments.charge(card, this.prices.of(courseId));
this.repo.save(userId, courseId);
this.mailer.confirm(userId);
}
}
// Composition root - chon implementation o MOT noi
const service = new EnrollmentService(
new EnrollmentValidator(),
new StripeGateway(), // doi sang MoMoGateway -> chi sua o day
new SqlEnrollmentRepo(),
new SmtpMailer(),
new SqlPriceLookup(),
); - ▸Đổi Stripe → MoMo, SQL → Mongo, SMTP → SendGrid: chỉ sửa ở composition root.
- ▸Thêm cổng thanh toán mới = thêm một lớp implement PaymentGateway (OCP).
- ▸Nghiệp vụ enroll() không còn biết chi tiết nào - sạch và ổn định.
enrollment.test.ts - tiêm fake, không chạm dịch vụ thật
class FakeGateway implements PaymentGateway {
charged: number[] = [];
charge(_card: Card, amount: number) { this.charged.push(amount); }
}
it("charge dung gia cua khoa hoc", () => {
const gw = new FakeGateway();
const service = new EnrollmentService(
new EnrollmentValidator(), gw,
new FakeRepo(), new FakeMailer(),
{ of: () => 499000 }, // PriceLookup gia lap
);
service.enroll("u1", "c1", card);
expect(gw.charged).toEqual([499000]);
}); - ▸Test chạy nhanh, không charge thẻ/ghi DB/gửi mail thật.
- ▸Kiểm được từng hành vi (đã charge đúng giá) một cách cô lập.
- ▸So với ban đầu (không thể test): đây là lợi ích rõ nhất của SOLID.
Checklist hoàn thành
Câu hỏi thường gặp
Trước tiên VIẾT TEST cho hành vi hiện tại (characterization test) để có "lưới an toàn" - bảo đảm refactor không đổi hành vi. Sau đó đi từng bước nhỏ: tách một trách nhiệm, chạy test, commit; lặp lại. Đừng đập đi xây lại một lần - rủi ro rất cao.
Không. Thường bắt đầu bằng SRP (tách trách nhiệm) vì nó mở đường cho mọi thứ khác. Tách xong, việc đưa interface + tiêm phụ thuộc (DIP) và cho phép mở rộng (OCP) trở nên tự nhiên. Áp tới đâu thấy ĐỦ thì dừng - đừng cố nhồi cho đủ chữ.
Bài O - Open/Closed: mở rộng không sửa code cũ →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.
Trước khi refactor một module lớn, việc an toàn nên làm đầu tiên là gì?
- 1
Liệt kê vi phạm
Với lớp
EnrollmentService"bẩn" ở Bước 1, liệt kê mọi vi phạm SOLID và giải thích ngắn từng cái.✅ Hoàn thành khi: Chỉ ra ≥3 vi phạm (vd SRP, DIP, OCP) đúng chỗ, kèm lý do.
- 2
Lưới an toàn trước
Viết một characterization test cho hành vi hiện tại của module (của mèo con hoặc ví dụ trong bài) trước khi đụng vào.
✅ Hoàn thành khi: Có ít nhất một test chạy xanh mô tả hành vi hiện tại; dùng làm mốc khi refactor.
- 3
Refactor từng bước
Refactor
EnrollmentService: (1) tách trách nhiệm theo SRP; (2) đưa interface + tiêm phụ thuộc theo DIP; (3) cho phép thêm cổng thanh toán theo OCP.✅ Hoàn thành khi: Mỗi bước commit riêng; test luôn xanh; nghiệp vụ không còn new chi tiết cụ thể.
- 4
Dự án của mèo con
Áp dụng đúng quy trình trên cho một module THẬT trong dự án của mèo con; viết bộ unit test cho các phần đã tách.
✅ Hoàn thành khi: Trước/sau rõ ràng; ≥1 phần test bằng bản giả; nêu được lợi ích & đánh đổi.