Free

Để Claude làm refactor

Refactor không có triệu chứng — Claude sai bạn không thấy. Ba hàng rào: test, commit nguyên tử, click tay.


Refactor là task Claude nguy hiểm nhất. Bug có triệu chứng — nút không phản ứng, giá trị undefined, stack trace — nên bạn biết được fix của Claude có đúng không. Refactor thì không. "Vẫn chạy" có thể nghĩa là "test vẫn pass" trong khi hành vi đã âm thầm thay đổi, bạn không nhận ra, và một tuần sau production nổ.

Gần đây tôi làm một refactor khá lớn với Claude ở how2claude: chuyển thanh toán crypto x402 từ PaymentHandler + FacilitatorClient tự viết (139 dòng) sang gem x402-rails, đồng thời tách phần map field của Purchase.create! / Subscription.create! — bị lặp ở hai controller — thành class method của model. Một commit, 4 file thay đổi, 2 xoá, 2 thêm.

Prompt của tôi là một từ: "refactor."

Có thể ngắn như vậy vì xung quanh có hàng rào bảo vệ.

Hàng rào #1: Không refactor khi chưa có test

Đến lúc branch này tới đó, có 221 test. Tất cả đường đi quan trọng của payment flow đều được phủ.

Hành động mặc định của Claude trước refactor không phải "xem test trước." Nên tôi bảo nó chạy bin/rails test trước, xác nhận xanh, rồi mới chạm vào gì.

Sau refactor, chạy lại. Vẫn xanh. Cái đó không nghĩa là không có regression — nghĩa là hành vi đã biết không bị hỏng.

Nếu codepath của bạn không có test phủ, bảo Claude viết một test tối thiểu khoá hành vi hiện tại. Chạy nó, commit. Rồi mới refactor. Nếu không, cái nó đang làm không phải refactor, mà là rewrite — và bạn không có cách nào xác minh tính tương đương.

Hàng rào #2: Bắt nó tách thay đổi thành commit nguyên tử

Refactor này thực ra là hai việc:

  1. Backend x402: tự viết → gem
  2. Map field của Purchase / Subscription: controller → class method của model

Phía frontend có việc thứ ba: viết lại luồng ký bên JS bằng viem + x402-fetch.

Tôi bảo Claude tách theo biên tự nhiên: backend + model extraction một commit (9f3e239), frontend commit riêng (93746d8). Mỗi commit có mô tả đầy đủ, danh sách file thay đổi, và lý do đổi.

Lợi ích:
- Diff dễ đọc. Một commit, một việc.
- Độ hạt rollback có kiểm soát. Nếu production gặp bug frontend, git revert 93746d8 chỉ revert frontend, backend giữ nguyên.
- Sự chú ý của Claude cũng tập trung. Một commit một việc — sự chú ý của nó cũng chỉ phủ việc đó.

Hàng rào #3: Đọc diff trước khi nói xong

Refactor xong, tôi bảo Claude dừng lại và đưa git diff --staged cho tôi xem. Không chạy test, không chạy app, đọc diff trước đã.

Các tín hiệu tôi quét:

  • Nó xoá cái gì? app/services/x402/payment_handler.rb bị xoá sạch — OK, đó chính là mục đích của việc chuyển sang gem. Nhưng nếu nó xoá thứ tôi không bảo động tới, tôi dừng lại và hỏi.
  • Map field có đổi không? Purchase.create!(wallet_address: verify_result["payer"], ...)Purchase.record_x402!(payment:, settlement:) giờ đọc payment[:payer]. Nguồn đã đổi (request.env của gem vs giá trị trả về của client cũ), nhưng các field phải ánh xạ một-một.
  • Thay đổi "tiện tay." Claude rất thích sửa mấy thứ "trông sai sai" khi đang refactor — sửa lời error message, đổi tên biến, tách một method mà nó thấy nên tồn tại. Cẩn thận mấy cái này. Lời hứa của refactor là "tương đương hành vi." Sửa tiện tay là phá lời hứa đó.

Hai cái bẫy Claude dính lần này

Bẫy 1: Stimulus controllers của gem âm thầm không load

Gem x402-rails đi kèm Stimulus controllers riêng. Claude viết code, test xanh hết. Tôi click tay nút thanh toán — không phản ứng.

Lý do: config/importmap.rb có pin cho @hotwired/stimulus trỏ tới một file vendor không tồn tại, và importmap âm thầm bỏ pin đó. Controllers của gem không bao giờ được load. Test không bắt được, vì bin/rails test không thực thi JS.

Bẫy 2: YAML parse 0x... thành integer

wallet_address: 0x833589... — không có dấu ngoặc. YAML thấy tiền tố 0x, đọc thành số nguyên hệ 16. Facilitator nhận một non-string và từ chối. Claude không dừng lại nghĩ về quy tắc parse YAML khi viết config.

Cả hai bẫy đều được bắt vì tôi click nút thật. Test pass không phải là feature hoạt động. Xác minh tay sau refactor không thể bỏ qua.

Quy trình đầy đủ của refactor với Claude

  1. Chạy toàn bộ test suite. Xác nhận xanh. Nếu code mục tiêu không có phủ, bảo Claude viết test tối thiểu khoá hành vi hiện tại, rồi commit.
  2. Nói rõ cái surface muốn trích. "Refactor" đủ dùng khi sự lặp lại rõ ràng; không rõ thì nói "trích map field của X thành class method ở Y."
  3. Tách commit nguyên tử. Một commit một việc.
  4. Tự đọc diff. Cái gì bị xoá, map field có đứng vững, có sửa tiện tay gì không.
  5. Chạy test lại. Xanh.
  6. Nếu chạm vào đường user nhìn thấy, click qua feature bằng tay. Những lớp test không chạm tới — JS, importmap, CDN, parse YAML — bạn phải nhìn bằng mắt mình.

Refactor là tình huống "để Claude cầm lái" rủi ro cao nhất. Hàng rào không phải cho Claude. Chúng cho bạn — để khi Claude sai, bạn bắt được trong 5 phút thay vì khi production đang cháy.