Tái Cấu Trúc Code Cũ
Mọi dự án outsourcing chạy dài đều tích lũy nợ kỹ thuật. Controller phình lên 800 dòng, business logic bị chôn vùi trong view template, validation trùng lặp nằm rải rác khắp 12 file. Khách hàng biết đây là vấn đề — họ cảm nhận được mỗi khi một “thay đổi đơn giản” mất cả tuần. Nhưng tái cấu trúc mà không có test, không có kế hoạch, và với một team đã rotate sáu tháng trước là cách bạn đưa vào regression làm mất đi mối quan hệ đó. Sun Agent Kit scout nợ kỹ thuật, xây dựng kế hoạch tái cấu trúc dần dần với lưới an toàn test, và thực thi từng bước trích xuất theo trình tự giữ ứng dụng luôn chạy được.
Tổng Quan
Mục tiêu: Trích xuất và tái cấu trúc có hệ thống nợ kỹ thuật tích lũy trong một dự án chạy dài mà không làm hỏng chức năng hiện có
Thời gian: 1–3 giờ cho một module tập trung (so với 1–3 ngày tái cấu trúc thủ công rủi ro)
Agent sử dụng: scout, planner, implementer, tester, reviewer
Lệnh: /sk:scout, /sk:plan, /sk:cook, /sk:test, /sk:code-review
Yêu Cầu Trước Khi Bắt Đầu
- Sun Agent Kit đã được cài đặt và xác thực (hướng dẫn cài đặt)
- Git repository với working tree sạch (commit hoặc stash mọi thứ trước khi bắt đầu)
- Ít nhất một phần test suite — ngay cả coverage thấp còn hơn là không có gì
- Rõ ràng về module hoặc khu vực tính năng nào đang được nhắm đến (không cố gắng “tái cấu trúc mọi thứ”)
Quy Trình Từng Bước
Bước 1: Scout nợ kỹ thuật và xây dựng debt map
Trước khi chạm vào một dòng code, hãy có một bức tranh đầy đủ về những gì bị hỏng và tại sao.
/sk:scout "large classes, long methods, duplicated logic, missing tests, god objects, business logic in controllers, raw SQL in views"
Điều gì xảy ra: Agent thực hiện:
- Quét codebase để tìm các chỉ số nợ kỹ thuật cấu trúc — file quá lớn, method dài, khối code trùng lặp
- Xác định god object (class xử lý nhiều concern không liên quan)
- Phát hiện dependency tangle và circular reference
- Tạo báo cáo debt map lưu vào
plans/reports/
Bước 2: Lập kế hoạch trình tự tái cấu trúc
Agent tạo một kế hoạch có thứ tự tôn trọng các phụ thuộc — bạn không thể trích xuất một interface phụ thuộc vào một class bạn chưa cô lập.
/sk:plan "Refactor OrderService.js — extract pricing, inventory, and notification concerns into separate services. Preserve all existing behavior."
Điều gì xảy ra: Agent thực hiện:
- Phân tích dependency graph của file mục tiêu — method, external dependency, và call site
- Xác định những concern nào có thể trích xuất độc lập
- Sắp xếp các bước trích xuất để mỗi bước an toàn (không có circular dependency giữa chừng)
- Lưu kế hoạch với các bước ước tính vào
plans/
Bước 3: Thêm characterisation test trước khi chạm vào production code
Đây là lưới an toàn. Characterisation test khóa lại hành vi hiện tại — chúng không test những gì code nên làm, mà test những gì nó thực sự đang làm. Các regression trở nên không thể bỏ sót.
/sk:cook "Add characterisation tests for OrderService.js — cover all public methods, capture current return values and side effects including edge cases"
Điều gì xảy ra: Agent thực hiện:
- Phân tích tất cả public method và lập danh mục side effect (DB write, email, log)
- Tạo bộ test nắm bắt hành vi hiện tại với spy setup cho side effect
- Chạy test được sinh ra trên code chưa sửa đổi để xác nhận baseline pass
Bước 4: Trích xuất service đầu tiên (PricingService)
Trích xuất từng concern một lần. Mỗi lần trích xuất đủ nhỏ để review và revert nếu cần.
/sk:cook "Extract PricingService from OrderService.js — move calculateTotal(), applyDiscount(), calculateTax(), formatCurrency() into src/services/PricingService.js"
Điều gì xảy ra: Agent thực hiện:
- Chuyển các method được chỉ định vào file service mới
- Cập nhật file gốc để delegate sang service mới qua dependency injection
- Chạy characterisation test để xác nhận không có thay đổi hành vi
- Chạy bộ test đầy đủ để xác minh không có regression
Bước 5: Lặp lại trích xuất cho các concern còn lại
Chạy cùng pattern cho InventoryService và NotificationService. Mỗi bước mất vài phút và bộ test xác nhận từng cái.
/sk:cook "Extract InventoryService from OrderService.js — move checkStock(), reserveStock(), releaseStock() into src/services/InventoryService.js"
/sk:cook "Extract NotificationService from OrderService.js — move sendOrderConfirmation(), sendShippingUpdate(), sendCancellationEmail() into src/services/NotificationService.js"
Điều gì xảy ra: Với mỗi lần trích xuất, agent chuyển method, cập nhật delegation, và chạy cả characterisation test lẫn bộ test đầy đủ để xác nhận không có regression.
Bước 6: Review code đã tái cấu trúc và commit
Sau khi tất cả các trích xuất xong, chạy code review để xác nhận cấu trúc đã tái cấu trúc khớp với kế hoạch.
/sk:code-review --pending
Điều gì xảy ra: Agent thực hiện:
- Review các thay đổi cấu trúc — giảm kích thước file, file service mới, phân tách concern
- Kiểm tra dependency injection nhất quán trên tất cả service đã trích xuất
- Xác nhận không có code smell mới nào được đưa vào
- Tạo tóm tắt với mức độ giảm nợ kỹ thuật đạt được
Ví Dụ Hoàn Chỉnh: Monolith 2 Năm Tuổi Cần Trích Xuất Module
Tình huống
Một khách hàng fintech Việt Nam có một Laravel monolith hai năm tuổi bắt đầu như một công cụ quản lý vay đơn giản và trở thành, qua các lần bổ sung tính năng liên tiếp bởi bốn team phát triển khác nhau, một codebase lớn trong đó LoanController xử lý đăng ký vay, credit scoring, lập lịch trả nợ, thông báo SMS, tạo PDF, và nhập journal kế toán. Team hiện tại (rotate vào sáu tháng trước) sợ không dám đụng vào — mỗi thay đổi lại làm hỏng điều gì đó không ngờ tới. Khách hàng muốn một mobile API mới, nhưng controller không thể được mở rộng an toàn trong trạng thái hiện tại.
Chuỗi lệnh sử dụng
# Tuần 1 Ngày 1 — hiểu toàn bộ phạm vi nợ kỹ thuật
/sk:scout "large classes, duplicated logic, missing tests, god objects, business logic in controllers, circular dependencies"
# Tạo kế hoạch tái cấu trúc theo giai đoạn
/sk:plan "Create phased refactoring plan for LoanController.php — extract CreditScoringService, RepaymentService, NotificationService, PDFService, AccountingService"
# Ngày 2 — lưới an toàn trước tiên
/sk:cook "Add characterisation tests for LoanController — cover all public methods, capture all side effects"
# Ngày 3 — bắt đầu với service ít phụ thuộc nhất
/sk:cook "Extract CreditScoringService from LoanController — move all credit evaluation logic"
# Ngày 4 — trích xuất tiếp theo
/sk:cook "Extract RepaymentService from LoanController — move schedule generation, payment processing, late fee calculation"
# Tuần 2 — tiếp tục trích xuất
/sk:cook "Extract NotificationService from LoanController — SMS and email handling, templating"
/sk:cook "Extract PDFService from LoanController — loan agreement and statement generation"
/sk:cook "Extract AccountingService from LoanController — journal entry creation, GL posting"
# Cuối cùng — review tái cấu trúc hoàn chỉnh
/sk:code-review --pending
# Tài liệu kiến trúc mới cho team
/sk:docs "Generate architecture documentation for the new service layer — class diagram, dependency graph, how to add a new loan type"
Kết quả
Qua hai tuần, LoanController co lại từ một god object khổng lồ xuống còn một lớp orchestration mỏng. Năm service chuyên biệt có thể test độc lập. API endpoint mobile mới được một junior engineer chưa từng đụng vào code thêm vào trong vài giờ — dùng trực tiếp các service đã trích xuất. Nhận xét của khách hàng: “Cảm giác như một codebase khác hẳn.”
So Sánh Thời Gian
| Tác vụ | Thủ công | Với Sun Agent Kit |
|---|---|---|
| Lập bản đồ nợ kỹ thuật trong codebase | 4 giờ | vài phút |
| Lập kế hoạch trình tự trích xuất (an toàn) | 2 giờ | vài phút |
| Viết characterisation test | 3 giờ | vài phút |
| Mỗi lần trích xuất service + xác minh | 60 phút mỗi lần | vài phút mỗi lần |
| Code review kết quả đã tái cấu trúc | 60 phút | vài phút |
| Cập nhật tài liệu team | 90 phút | vài phút |
| Tổng cộng (5 service trích xuất) | ~16 giờ | ~2 giờ |
Thực Hành Tốt Nhất
1. Viết characterisation test trước bất kỳ trích xuất nào ✅
Bộ characterisation test là cách duy nhất đáng tin cậy để biết bạn chưa làm hỏng gì. Đừng bắt đầu trích xuất mà không có nó. Một bộ test mất vài phút để sinh ra tốt hơn vô hạn so với phát hiện một regression trong production ba tuần sau.
2. Trích xuất một concern mỗi PR ✅
PR nhỏ, tập trung là có thể review được. Một PR vừa trích xuất PricingService vừa trích xuất InventoryService vừa tái cấu trúc controller vừa thêm test là không thể review — đó là merge và hy vọng. Mỗi lần trích xuất là một đơn vị logic: một service, một PR, một review, một merge. Kế hoạch do agent sinh ra được sắp xếp đúng theo cách này.
3. Đừng tái cấu trúc và thêm tính năng mới trong cùng một branch ❌
Sự cám dỗ là “dọn dẹp khi tôi đang ở đây.” Điều này kết hợp hai thay đổi độc lập và làm cho regression không thể quy kết được. Nếu một bug được tìm thấy trong branch tái cấu trúc + tính năng, bạn không biết cái nào gây ra nó. Giữ branch tái cấu trúc thuần túy — không có behavior mới, chỉ tái cấu trúc.
4. Đừng bỏ qua bước scout vì bạn “đã biết nợ kỹ thuật” ❌
Mọi developer đã làm việc trong codebase sáu tháng đều nghĩ họ biết tất cả vấn đề. Scout một cách đáng tin cậy làm lộ ra nợ kỹ thuật mà sự quen thuộc che giấu — logic trùng lặp nằm rải rác khắp các file do các engineer khác nhau viết, hoặc circular dependency giải thích tại sao một thay đổi “đơn giản” cần đụng vào nhiều file. Đọc debt map trước khi lập kế hoạch.
Xử Lý Sự Cố
Vấn đề: Characterisation test thất bại trên code chưa sửa đổi
Giải pháp: Điều này có nghĩa là các test hiện tại có phụ thuộc môi trường — trạng thái database, API call bên ngoài, hoặc logic nhạy cảm với thời gian — mà generator characterisation không tính đến. Chạy /sk:cook "Fix characterisation test environment setup — add database seeding and mock external calls". Mục tiêu là một bộ test xác định có thể chạy trên bất kỳ máy nào.
Vấn đề: Sau khi trích xuất, integration test thất bại mặc dù unit test pass
Giải pháp: Trích xuất đôi khi tiết lộ rằng hai service dùng chung state thông qua một biến module-level hoặc singleton không hiển thị ở cấp unit. Chạy /sk:debug "integration test failures after service extraction" và mô tả lỗi — agent sẽ xác định shared state và đề xuất inject nó một cách tường minh hay chuyển nó sang configuration layer.
Vấn đề: Service đã trích xuất có quá nhiều constructor parameter (constructor bloat)
Giải pháp: Bảy hoặc nhiều hơn constructor parameter trên một service là dấu hiệu bản thân service đó cần được tách thêm. Chạy /sk:plan "split [ServiceName] — identify sub-concerns" để lấy kế hoạch trích xuất cấp hai. Agent sẽ xác định những nhóm method nào trong service tự nhiên gom cụm với nhau.
Vấn đề: CI pipeline của khách hàng thất bại sau khi merge refactoring do thay đổi import path
Giải pháp: Chạy /sk:cook "Update all import paths after service extraction — find and replace old module references" với danh sách các file đã di chuyển. Agent sinh ra các cập nhật cho mỗi import statement trong codebase.
Bước Tiếp Theo
- Đánh Giá Code — Đưa mỗi PR trích xuất qua code review trước khi merge
- Tối Ưu Hiệu Suất — Sau khi tái cấu trúc, profile code đã được tái cấu trúc để xác nhận không có tác động hiệu suất ngoài ý muốn
- Tạo Tài Liệu Tự Động — Tài liệu hóa kiến trúc mới để lần rotation team tiếp theo bắt đầu với bản đồ, không phải bí ẩn
Điểm mấu chốt: Code cũ không được tái cấu trúc bằng cách viết lại mọi thứ cùng một lúc — nó được tái cấu trúc dần dần, từng service được trích xuất một lần, với lưới an toàn test đảm bảo không có gì bị hỏng trên đường đi.