Tuần trước, một trong những sự kiện công nghệ lớn nhất năm đã khép lại, và tôi nhận ra mình chưa hề chia sẻ về chủ đề mà tôi đã trình bày. Với những kiến thức vẫn còn tươi mới, hãy cùng nhau đào sâu vào một vấn đề muôn thuở trong phát triển phần mềm: quản lý và di chuyển các ứng dụng kế thừa (legacy applications).
Thành thật mà nói, dù trải nghiệm tham gia các hội nghị trực tuyến mang lại nhiều thuận tiện, nó vẫn không thể sánh bằng việc trực tiếp có mặt, trao đổi ý kiến cùng những chuyên gia hàng đầu. Thay vì có thể nhâm nhi cà phê (hoặc bia 😉) cùng những bộ óc vĩ đại, tôi lại theo dõi bài nói của chính mình từ căn bếp và trả lời tin nhắn công việc. Tuy nhiên, đó vẫn là một trải nghiệm học hỏi tuyệt vời. Ai biết được, có thể một ngày nào đó, tôi sẽ có cơ hội trình bày trực tiếp tại những sự kiện mang tầm cỡ quốc tế.
[](https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdvk1lia6g4dcdstvdpvp.png)
Dù sao thì, hãy quay lại với chủ đề chính. Bài nói của tôi có tên **”Rewrite hay Refactor? Di chuyển Ứng Dụng Kế Thừa An Toàn sang Framework Hiện Đại như thế nào?”**. Tôi tin rằng mình là người phù hợp để thảo luận về vấn đề này. Tại sao ư? Bởi vì tôi đã thực hiện di chuyển rất nhiều ứng dụng frontend trong suốt sự nghiệp của mình, với đủ mọi cấu hình phức tạp: từ Angular cũ sang Angular hiện đại, từ ASP.NET MVC 5 sang Vue, từ React với class components sang React hiện đại với hooks, và vô số trường hợp khác.
Lý do rất đơn giản: khi một công ty cấp bách cần di chuyển hệ thống và nhận được CV từ một chuyên gia đã có kinh nghiệm trong lĩnh vực này, họ thường rất sẵn lòng tuyển dụng—thậm chí với mức lương cao hơn thị trường một chút. 😄 Đó là cách tôi trở thành một chuyên gia di chuyển frontend, và chắc chắn tôi có vài điều muốn chia sẻ về nó.
Mục lục
Tại Sao Phải Di Chuyển Ứng Dụng Kế Thừa? Nỗi Đau Và Sự Sống Còn Của Sản Phẩm
Trước khi đi sâu vào các chiến lược, chúng ta cần trả lời một câu hỏi cơ bản hơn: tại sao phải di chuyển ứng dụng kế thừa ngay từ đầu?
Tôi đã nghe câu hỏi này nhiều lần từ các bên liên quan, đặc biệt là các chủ sản phẩm không có nền tảng kỹ thuật. Nếu ứng dụng vẫn hoạt động tốt, tại sao phải động vào nó? Các nhà phát triển chỉ đang chạy theo xu hướng và muốn đưa những công nghệ bóng bẩy vào CV của họ, đúng không?
Câu trả lời của tôi thường rất đơn giản và trung thực.
Tôi không phải là một “fanboy” của bất kỳ framework cụ thể nào. Đối với tôi, chúng chỉ là những công cụ. Giống như những cây búa. Nhưng khi xây dựng một thứ gì đó, tôi thà dùng một cây búa chắc chắn, đáng tin cậy còn hơn một cây búa gỉ sét với những bộ phận bị thiếu. 😉🔨⛏️🪓🔧🪛
Nhưng việc di chuyển không chỉ dừng lại ở trải nghiệm của nhà phát triển. Trên thực tế, đó là về sự sống còn của sản phẩm của bạn. Đặc biệt là trong bối cảnh hiện nay, khi công nghệ phát triển nhanh hơn bao giờ hết và Trí tuệ nhân tạo (AI) đang tăng tốc mọi thứ lên một tầm cao mới.
Hãy lấy ví dụ về bảo mật. Khi một thư viện trở nên lỗi thời và không còn ai duy trì nó, các bản vá bảo mật sẽ ngừng được cung cấp. Các báo cáo về lỗ hổng bảo mật bắt đầu chuyển sang màu đỏ. Không một chủ sản phẩm nào trong lịch sử từng nói: “Chắc chắn rồi, hãy hy sinh bảo mật để có thêm vài tính năng!” Ít nhất thì tôi hy vọng là vậy. 😅
Và đừng quên các công cụ phát triển hiện đại. Chỉ riêng việc sử dụng các công cụ build như Vite đã có thể tạo ra sự khác biệt lớn. Các ứng dụng hiện đại đơn giản là nhanh hơn, và ứng dụng nhanh hơn là ứng dụng tốt hơn.
Có rất nhiều lý do khác, nhưng điều quan trọng nhất cần nhớ là:
Càng trì hoãn việc di chuyển, nó sẽ càng khó khăn và tốn kém hơn.
Nghiên cứu từ các công ty công nghệ lớn như Microsoft và Google đã chỉ ra rằng chi phí bảo trì và rủi ro bảo mật của các hệ thống cũ tăng theo cấp số nhân theo thời gian. Việc đầu tư vào di chuyển không chỉ là nâng cấp công nghệ mà còn là một chiến lược kinh doanh thiết yếu để đảm bảo tính cạnh tranh và bền vững cho sản phẩm.
Chiến Lược Di Chuyển: Rewrite (Big Bang) Hay Refactor (Strangler Pattern)?
Thật thú vị, sự ra đời của các mô hình ngôn ngữ lớn (LLMs) như ChatGPT đã không thay đổi đáng kể các chiến lược di chuyển cơ bản. Chúng về cơ bản vẫn giống như trước khi AI trở nên phổ biến. Đôi khi công việc nhanh hơn, đôi khi không, nhưng các cách tiếp cận cốt lõi vẫn không thay đổi.
Có lẽ có nhiều chiến lược di chuyển như có số lượng kỹ sư cấp cao, nhưng trên thực tế, chúng thuộc hai loại chính:
1. Rewrite (Chiến lược Big Bang)
Đây là việc viết lại toàn bộ ứng dụng — hoặc, như thường xảy ra với frontend, nâng cấp trực tiếp một stack cũ kỹ lên phiên bản framework mới nhất.
2. Refactor (Strangler Pattern – Mô hình cây si)
Tái cấu trúc ứng dụng từng phần một trong khi vẫn tiếp tục phát triển và cung cấp tính năng mới.
Vậy, cách tiếp cận nào tốt hơn?
Thành thật mà nói, không cần phải tranh cãi gay gắt về điều này. Trong hầu hết các trường hợp, thực tế của dự án sẽ quyết định cho bạn.
Nếu ứng dụng của bạn tương đối nhỏ, đội ngũ của bạn có kinh nghiệm, tài liệu khá tốt và bạn có thể tạm dừng phát triển tính năng trong vài tuần hoặc vài tháng, thì việc viết lại hoàn toàn (rewrite) có thể là một lựa chọn hợp lý.
Mặt khác, nếu bạn đang làm việc trên một dự án khổng lồ với nhiều nhà phát triển cấp dưới hoặc những người không quen thuộc với công nghệ, và tài liệu gần như không tồn tại — thì việc tái cấu trúc tăng dần (incremental refactor) có lẽ là lựa chọn thực tế duy nhất của bạn.
Tất nhiên, có nhiều yếu tố khác cần xem xét. Dưới đây là bảng tóm tắt nhanh:
| Yếu tố | Rewrite (Big Bang) | Refactor (Strangler) |
| :——————- | :—————————————————— | :—————————————————————– |
| Kích thước ứng dụng | Nhỏ hoặc vừa | Lớn |
| Phát triển tính năng | Có thể tạm dừng | Phải tiếp tục |
| Tài liệu | Tốt | Kém |
| Kinh nghiệm nhóm | Cao | Pha trộn |
| Mức độ chấp nhận rủi ro | Cao hơn | Thấp hơn |
| Thời gian có kết quả đầu tiên | Ngắn | Dài |
| Độ phức tạp trong quá trình di chuyển | Thấp hơn | Cao hơn |
| Rủi ro di chuyển không hồi kết | Thấp | Cao |
Rewrite (Big Bang): Không Đáng Sợ Như Bạn Tưởng
Hãy cùng tìm hiểu sâu hơn về cả hai cách tiếp cận, bắt đầu với Big Bang.
Tôi phải thừa nhận — tôi thích chiến lược này. Chắc chắn, sách và các bài viết thường mô tả nó là rủi ro, đôi khi thậm chí là một “anti-pattern” (chống mẫu). Và họ đúng — nếu chúng ta đang nói về một monolith Java hai mươi năm tuổi mà không ai dám chạm vào.
Nhưng nhiều ứng dụng frontend tương đối mới và tương đối nhỏ. Trong những tình huống đó, cách tiếp cận Big Bang có thể đơn giản là nhanh hơn và rẻ hơn.
Viết lại hoàn toàn ngày nay tương đối hiếm. Tuy nhiên, các bản nâng cấp lớn khá phổ biến: ví dụ, chuyển từ Angular 7 sang Angular hiện đại với Signals, hoặc từ React cũ với class components sang React hiện đại với hooks.
Những dự án này vẫn đòi hỏi rất nhiều công việc và đào sâu vào mã cũ, nhưng bạn sẽ đạt được mục tiêu nhanh hơn nhiều so với việc di chuyển theo mô hình Strangler.
Tôi sẽ không mô tả các giai đoạn lập kế hoạch, script di chuyển, codemods, hoặc các công cụ cụ thể mà tôi đã sử dụng. Những chi tiết đó thay đổi tùy theo từng dự án và không có nhiều ý nghĩa trong một bài viết chung.
Tuy nhiên, có những điều nhất quán một cách đáng ngạc nhiên trong hầu hết mọi quá trình di chuyển. 😉
Ví dụ Thực Tế: Di Chuyển Big Bang từ Angular 7 lên Angular Mới Nhất
Đây là một ví dụ thú vị. Đôi khi chúng ta tự hỏi làm thế nào một nhóm có thể để một ứng dụng tồn tại nhiều năm mà không nâng cấp. Trên thực tế, điều đó đáng ngạc nhiên là dễ dàng, đặc biệt là khi không ai chủ động làm việc trên nó vì sản phẩm đang trong chế độ bảo trì và không thay đổi đáng kể trong nhiều năm.
Đó chính xác là trường hợp ở đây.
Sau đó, một ngày nọ, các bên liên quan quyết định mở rộng ứng dụng đáng kể và thêm một số tính năng mới. Tôi đã thuyết phục họ nâng cấp lên phiên bản Angular mới nhất, chủ yếu vì lý do bảo mật — hệ thống lưu trữ dữ liệu quan trọng.
Nói tóm lại, bốn nhà phát triển đã nâng cấp nó lên phiên bản mới nhất trong bốn tháng. Và nó hóa ra khó hơn nhiều so với tôi mong đợi.
Trước hết, rất dễ đánh giá thấp phạm vi của vấn đề. Từ bên ngoài, nó trông đơn giản: “Chỉ cần nâng cấp Angular.”
Nhưng bản thân framework hiếm khi là vấn đề thực sự. Hệ sinh thái xung quanh nó mới là vấn đề.
Ví dụ, thư viện component chính của chúng tôi đã giới thiệu những thay đổi cú pháp lớn ở đâu đó giữa các phiên bản 12 và 13. Hãy tưởng tượng có bao nhiêu nơi cần được cập nhật! Chắc chắn, AI có thể giúp, nhưng nếu một kỹ sư UI đầy tham vọng quyết định đổi tên các lớp CSS và thay đổi cấu trúc component, AI sẽ không cứu được bạn — bạn sẽ phải tự mình sửa chữa mọi thứ thủ công.
Một số thư viện bên thứ ba đã bị bỏ rơi từ lâu. Và đây là điều đáng nhớ:
Nếu bạn có một thư viện không được cập nhật trong nhiều năm, mà tác giả của nó dường như đã quên mất sự tồn tại của nó, và nó thậm chí không còn biên dịch trên Node 18 nữa, thì đó không phải là "mã đang hoạt động". Đó là một quả bom hẹn giờ.
Và vâng, chúng tôi có rất nhiều bài kiểm tra end-to-end. Nhiều trong số đó đã “nổ tung” vì những thay đổi trong thư viện component. 😄 Vì vậy, ngay cả các bài kiểm tra tự động cũng không phải lúc nào cũng cứu được bạn.
Một bài học quan trọng khác: luôn có một chiến lược phân nhánh (branching strategy) vững chắc cho các bản sửa lỗi nóng (hotfixes). Hãy giả định rằng điều gì đó sẽ sai. Bởi vì cuối cùng, điều gì đó có thể sẽ sai.
[](https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fadkp3fuljt82hwfm39ea.png)
May mắn thay, quá trình di chuyển đã thành công.
Thời gian build được cải thiện đáng kể. Kích thước bundle giảm đáng kể. Giao diện mới trông đẹp hơn nhiều so với giao diện cũ, vì vậy ngay cả những bên liên quan kháng cự công nghệ nhất cũng phải ấn tượng.
Và có lẽ quan trọng nhất, ứng dụng đã sẵn sàng cho các bản nâng cấp trong tương lai. Ngay cả nhóm QA — những người ban đầu ghét chúng tôi, cuối cùng cũng phải thừa nhận rằng việc nâng cấp thường xuyên tốt hơn nhiều so với việc thực hiện một lần mỗi bảy năm. 🤣
Di Chuyển Tăng Dần (Incremental Migration): Vừa Ăn Vừa Giữ Bánh
Tuy nhiên, đôi khi ứng dụng của bạn quá lớn để áp dụng cách tiếp cận Big Bang, hoặc bạn không thể ngừng cung cấp tính năng. Trong trường hợp đó, di chuyển tăng dần trở thành lựa chọn thực tế duy nhất.
Loại tái cấu trúc từng bước này thường được thực hiện bằng cách sử dụng **Strangler Pattern** (Mô hình cây si). Tất nhiên, có những cách tiếp cận khác, nhưng cá nhân tôi thích cách này vì nó thanh lịch, tương đối đơn giản và đã được thử nghiệm bởi một số công ty công nghệ lớn nhất thế giới.
Vậy, Strangler Pattern chính xác là gì?
Hãy xem sơ đồ đẹp mắt dưới đây, được tôi tự tay vẽ bằng draw.io. 😄
[](https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsdwj1b6njiqwowgvkck1.png)
Như bạn thấy, bạn bắt đầu với ứng dụng kế thừa của mình. Bạn xây dựng một ứng dụng mới song song với nó. Sau đó, bạn đặt một reverse proxy ở phía trước, nó sẽ quyết định tuyến đường nào sẽ đi đến ứng dụng nào.
Từ đó, bạn di chuyển từng màn hình, từng tuyến đường. Theo thời gian, ứng dụng kế thừa sẽ nhỏ dần cho đến khi, hy vọng, tất cả những gì còn lại là ứng dụng mới, và bạn cuối cùng có thể loại bỏ cả ứng dụng cũ và reverse proxy.
Trong kịch bản này, cả hai ứng dụng nên chia sẻ xác thực và backend, nhưng không chia sẻ mã frontend.
Và nếu bạn hoàn toàn phải làm cho hai ứng dụng giao tiếp với nhau, tôi strongly khuyến nghị sử dụng một cái gì đó đơn giản như các sự kiện DOM tùy chỉnh (custom DOM events) thay vì các “bộ chuyển đổi tạm thời” (temporary adapters) được cho là.
Bởi vì tất cả chúng ta đều biết “tạm thời” có nghĩa là gì trong kỹ thuật phần mềm. **Mãi mãi**. 😅 Và trước khi bạn biết điều đó, những bộ chuyển đổi đó trở thành một mảnh legacy hoàn toàn mới.
Ví dụ Thực Tế: Di Chuyển từ Backbone sang Vue với Strangler Pattern
Đây là một ứng dụng cũ được viết bằng Backbone — và tôi phải thừa nhận, nó thực sự được viết rất tốt. Tuy nhiên, nó vẫn là Backbone. 😄
Ứng dụng không quá lớn, vì vậy việc di chuyển Big Bang về lý thuyết là có thể. Nhưng chúng tôi là một startup, và một ngày nọ chúng tôi nghe thấy những lời nổi tiếng này: _”Các bạn ơi, chúng ta vừa bán một tính năng chưa tồn tại. Các bạn có ba tháng để giao nó. Chúc vui vẻ!”_
Phản ứng ngay lập tức của tôi là: _”Hahaha. Rất buồn cười. Không đời nào tôi xây dựng thứ đó bằng Backbone.”_ May mắn thay, các bên liên quan của chúng tôi đã thông minh và đồng ý với việc di chuyển theo mô hình Strangler.
Nhóm của tôi bao gồm tôi và… hai nhà phát triển Java cấp dưới, những người, tôi thề, lần đầu tiên nghe từ “TypeScript” trong đời.
May mắn thay, họ đầy tham vọng và học rất nhanh. Chà, họ cũng không có nhiều lựa chọn. 😄 Và đó là một lợi thế khác của chiến lược này: các nhà phát triển cấp dưới có thể học hỏi trong khi làm việc.
Tôi đã tạo một ứng dụng Vue mới ở bên cạnh. Rất nhiều người trong công ty đã biết Vue, vì vậy lựa chọn này là hiển nhiên.
Đầu tiên, tôi di chuyển màn hình đăng nhập làm bằng chứng khái niệm (proof of concept). Sau đó, chúng tôi chuyển sang tính năng mới sáng bóng mà khách hàng đã trả tiền. 😉
Trong năm tiếp theo, chúng tôi đã di chuyển ứng dụng từng màn hình một.
[](https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fuapvmn4gkw0wtc067rpw.png)
Cuối cùng, Backbone biến mất hoàn toàn. Và trong suốt quá trình, chúng tôi không gặp bất kỳ thời gian chết nào. Khách hàng thậm chí không nhận thấy rằng một quá trình di chuyển đang diễn ra.
Giống như trong ví dụ trước, bundle trở nên nhỏ hơn đáng kể, và thời gian build được cải thiện đáng kể.
Tuy nhiên, cách tiếp cận Strangler không phải lúc nào cũng màu hồng.
Trước hết, có yếu tố thời gian. Quá trình di chuyển mất rất nhiều thời gian. Trong trường hợp của chúng tôi, với một ứng dụng tương đối nhỏ, nó vẫn mất cả một năm.
Ngoài ra còn có nguy cơ **di chuyển không hồi kết**. Bạn có lẽ biết ý tôi là gì. Hạn chót chồng chất. Các tính năng cứ đến. Và sau một thời gian, bạn thấy mình gần như phải cầu xin chủ sản phẩm dành vài “story points” cho công việc di chuyển.
Và thật không may, không có cách nào để tránh chạm vào mã kế thừa. Không có cơ hội nào cả. Và tin tôi đi, các nhà phát triển ghét điều đó. Tôi đã nghe những câu như: _”Sylwia, tại sao bạn lại làm mọi thứ phức tạp như vậy? Bây giờ chúng ta phải biết hai ngăn xếp công nghệ!”_
Vì vậy, cho dù bạn chọn chiến lược nào, bạn có thể sẽ trở thành “vật tế thần” của nhóm. Ít nhất là trong khi quá trình di chuyển đang diễn ra.
Nhưng một khi nó kết thúc, kích thước bundle đã giảm đi một nửa, các báo cáo lỗ hổng không còn hiển thị màu đỏ, và khách hàng ngừng phàn nàn, bạn sẽ đột nhiên trở thành anh hùng của công ty. 😄
Vài Lời Cuối Cùng: Di Chuyển Ứng Dụng Kế Thừa – Không Khác Gì Cải Tạo Ngôi Nhà Cũ
Di chuyển các ứng dụng kế thừa giống như mua một ngôi nhà cũ vậy. Không phải lỗi của bạn khi nó đang xuống cấp, nhưng là trách nhiệm của bạn để đưa nó trở lại trạng thái tốt. Bạn có thể nghỉ vài tuần để cải tạo mọi thứ cùng một lúc, hoặc bạn có thể chuyển vào và xử lý từng phòng một.
Chiến lược tồi tệ nhất thực sự là không làm gì cả. Sớm hay muộn, bạn cũng sẽ phải di chuyển — chỉ là bạn sẽ làm điều đó vì một sự cố sản xuất mà thôi. 😉
Vậy, cách tiếp cận của bạn để “cải tạo” các ứng dụng kế thừa của mình là gì?
Disclaimer
Không có câu chuyện nào được mô tả ở trên thực sự xảy ra. Hoặc có lẽ chúng đã xảy ra? 😉
Là một chuyên gia và bị ràng buộc bởi các thỏa thuận bảo mật (NDA), tôi đã cố tình kết hợp nhiều câu chuyện di chuyển khác nhau để đảm bảo không công ty hay dự án nào có thể bị nhận diện. Điều đó không đặc biệt khó khăn, bởi vì sau một thời gian, tất cả các quá trình di chuyển bắt đầu trông giống nhau một cách đáng ngạc nhiên. 😄
Nếu bạn thích các bài viết của tôi, bạn cũng có thể theo dõi tôi trên [LinkedIn](https://www.linkedin.com/in/sylwia-laskowska-5a8467131/).



