Nếu Deadline Monster là áp lực bên ngoài, thì Legacy Code chính là “con rồng ngủ yên” bên trong hệ thống.
Ban đầu, nó không gây ra vấn đề gì – hệ thống vẫn chạy, tính năng vẫn hoạt động. Nhưng càng về sau, càng nhiều người chạm vào, càng nhiều deadline bồi đắp, con rồng ấy càng lớn dần. Đến một lúc, chỉ cần một thay đổi nhỏ cũng có thể làm nó bừng tỉnh và phun ra lửa: bug, downtime, khách hàng nổi giận.

Trong vai trò BRSE, bạn sẽ nhiều lần nghe dev team than thở:

  • “Code này không ai dám sửa.”
  • “Đụng vào là nổ bug.”
  • “Chúng ta cần viết lại từ đầu.”

Nhưng trong thực tế dự án, “viết lại từ đầu” gần như là một giấc mơ xa vời. Deadline không cho phép. Khách hàng không trả tiền cho việc đó. Vậy chúng ta làm gì?

 

Michael Feathers trong Working Effectively with Legacy Code cho chúng ta những lời khuyên:

  • Legacy không phải kẻ thù, mà là thứ cần được thuần hóa từng bước nhỏ.
  • Trước khi thay đổi, hãy bảo vệ hành vi hiện tại bằng test.
  • Đừng mơ cải tiến toàn bộ trong một lần – hãy cải tiến từng chỗ mình chạm vào.

Martin Fowler trong Refactoring nhấn mạnh rằng:

  • Mỗi dòng code có “smell” của nó. Nhận diện được, ta mới biết phải dọn dẹp ra sao.
  • Refactoring không phải là dự án lớn, mà là thói quen hàng ngày – giống như Kaizen trong code.

 

Đặc biệt, cùng là chỉnh sửa source code nhưng ta có thể chỉnh sửa một cách an toàn, ít ảnh hưởng nhất đến code legacy, để con rồng có thể ngủ yên.

🛠 Kỹ thuật sửa legacy code an toàn

1. Characterization Test

Ý tưởng: Khi code chưa có test, ta viết test để “đóng băng” hành vi hiện tại. Không cần hiểu đúng–sai nghiệp vụ, chỉ cần đảm bảo khi refactor không phá vỡ hành vi.

Ví dụ:

@Test

void testCalculateTotal_Characterization() {

    InvoiceService service = new InvoiceService();

    Invoice invoice = new Invoice(1000, “CUST01”);

    int result = service.calculateTotal(invoice);

    // Giữ nguyên hành vi cũ (dù công thức có thể sai)

    assertEquals(950, result);

}

📌 Sau này thay đổi công thức tính, nếu test fail → biết rằng hành vi đã đổi.

 

2. Sprout Method / Sprout Class

Ý tưởng: Thêm code mới vào legacy bằng cách viết method/class mới, rồi gọi từ code cũ. Tránh sửa trực tiếp code “dễ vỡ”.

Ví dụ thêm chức năng log (Sprout Method):

public class OrderProcessor {

    public void process(Order order) {

        // logic cũ…

        log(order);

    }

 

    // phương thức mới (sprout)

    private void log(Order order) {

        System.out.println(“Processing order “ + order.getId());

    }

}

👉 An toàn: rollback dễ dàng, test riêng log được.

 

3. Seam (Introduce Seam)

Ý tưởng: Một “điểm nối” cho phép thay đổi hành vi của code mà không cần sửa trực tiếp.
Cách thường dùng: Dependency Injection, Extract Interface, Factory.

Ví dụ:

public class OrderService {

    private final ShippingClient client;

 

    // Inject qua constructor (seam)

    public OrderService(ShippingClient client) {

        this.client = client;

    }

 

    public int calculateShippingFee(Order order) {

        return client.getFee(order.getDestination(), order.getWeight());

    }

}

👉 Trong test, ta inject FakeShippingClient.

 

4. Subclass & Override Method

Ý tưởng: Tạo subclass để “ghi đè” một phần hành vi, thay vì sửa trực tiếp class gốc.

Ví dụ:

public class OrderService {

    public int calculateFee(Order order) {

        return new ShippingApiClient().getFee(order.getDestination(), order.getWeight());

    }

}

 

class TestableOrderService extends OrderService {

    @Override

    public int calculateFee(Order order) {

        return 100; // giá trị giả để test

    }

}

 

5. Extract & Override Call

Ý tưởng: Nếu code gọi trực tiếp một phương thức khó test, ta extract nó ra method riêng, rồi override trong test.

Ví dụ:

public class InvoiceService {

    public int calculateTotal(Invoice invoice) {

        int discount = getDiscount(invoice);

        return invoice.getAmount() discount;

    }

 

    // Extracted call

    protected int getDiscount(Invoice invoice) {

        return new DiscountApi().getDiscount(invoice.getCustomerId());

    }

}

 

// Test: override getDiscount

InvoiceService testService = new InvoiceService() {

    @Override

    protected int getDiscount(Invoice invoice) {

        return 50; // fake

    }

};

6. Isolate Hard-to-test Dependencies

Ý tưởng: Với static util, new, I/O trực tiếp → tách ra, dùng seam hoặc wrapper.

Ví dụ (thay new bằng factory):

public class OrderService {

    private final Supplier<ShippingApiClient> clientFactory;

 

    public OrderService(Supplier<ShippingApiClient> clientFactory) {

        this.clientFactory = clientFactory;

    }

 

    public int calc(Order o) {

        return clientFactory.get().getFee(o.getDestination(), o.getWeight());

    }

}

 

7. Introduce Parameter Object (từ Refactoring)

Ý tưởng: Gom nhiều tham số liên quan thành 1 object → code rõ ràng, dễ test.

Ví dụ:

// Trước

calculateShippingFee(String destination, int weight, int length, int height, int width);

 

// Sau

calculateShippingFee(ShippingInfo info);

 

8. Replace Conditional with Polymorphism (Refactoring)

Ý tưởng: Nếu nhiều if/switch dựa trên type → tách ra subclass.

Ví dụ:

// Trước

if(order.getType().equals(“EXPRESS”)) { … } else { … }

 

// Sau

abstract class OrderType { abstract void process(Order o); }

class ExpressOrder extends OrderType { … }

class NormalOrder extends OrderType { … }

9. Encapsulate Collection

Ý tưởng: Tránh expose trực tiếp List/Map. Thay bằng API rõ ràng.

// Trước

public List<Item> items;

 

// Sau

public void addItem(Item i) { items.add(i); }

public List<Item> getItems() { return Collections.unmodifiableList(items); }

 

10. Rollback Strategy / Safe Change

Ý tưởng: Mỗi bước refactor nhỏ → có thể revert nhanh nếu lỗi. Kết hợp với characterization tests để biết khi nào hành vi thay đổi.

💡 Lời khuyên cho BRSE khi làm việc với Legacy Code

1. Luôn đặt an toàn lên hàng đầu

  • Nhắc nhở team dev: “Code cũ không có test = nguy cơ cao”.
  • Luôn yêu cầu có characterization test trước khi thay đổi.
  • Khi báo cáo với khách hàng, dùng ngôn ngữ nghiệp vụ: “Để giảm rủi ro (リスク低減), chúng tôi cần kiểm chứng hành vi hiện tại bằng test”.

2. Hiểu và truyền đạt về cách tiếp cận từng bước nhỏ

  • Thay vì yêu cầu “refactor toàn bộ”, nên giải thích: “Chúng ta sẽ cải tiến từng phần nhỏ, mỗi bước có thể rollback (切り戻し対応) nếu lỗi.”
  • Điều này tạo sự tin tưởng với khách hàng Nhật, vì họ quan tâm ổn định hơn tốc độ.

3. Đóng vai trò cầu nối ngôn ngữ và kỹ thuật

  • Với dev: dịch yêu cầu nghiệp vụ thành kỹ thuật an toàn (seam, sprout, mock dependency).
  • Với khách hàng: giải thích kỹ thuật thành ngôn ngữ dễ hiểu (ví dụ: “chúng tôi thêm lớp mới để tránh thay đổi trực tiếp code đang chạy trên production”).
  • Tránh nói chung chung “refactor”, mà nên cụ thể: “Tách chức năng ra để dễ kiểm soát” (機能分離).

4. Luôn hỏi “test đâu?”

  • Trước khi chấp nhận kế hoạch thay đổi, hãy kiểm tra xem có test bảo vệ chưa.
  • Nếu dev chưa viết test, nhấn mạnh: “Không có test thì không an toàn”.

5. Ghi nhớ rằng BRSE không cần code giỏi, nhưng phải hiểu đủ để đánh giá rủi ro

  • Bạn không cần viết test chi tiết, nhưng phải nhận diện được:
    • Phần nào “nguy hiểm” (phụ thuộc bên ngoài, static, I/O).
    • Khi nào cần seam hoặc sprout.
  • Điều này giúp bạn định hướng dev team và làm khách hàng yên tâm.

BRSE không chỉ là người “dịch” ngôn ngữ, mà là người bảo vệ sự an toàn của dự án.
Biết cách giải thích tầm quan trọng của test và các kỹ thuật xử lý legacy code = giúp khách hàng tin tưởng, dev team làm việc an toàn, dự án ít rủi ro.

Guest