Viết lại bằng Rails
(dirkjonker.bearblog.dev)- Sau khi cân nhắc nhiều stack khác nhau cho ứng dụng lập tờ khai hải quan, Ruby on Rails quen thuộc được dùng cho triển khai ban đầu vì cảm nhận là có thể tạo tính năng nhanh hơn rất nhiều so với các lựa chọn mới
- “version 2” dùng backend Rust và frontend SvelteKit chạy nhanh và trông đẹp, nhưng chỉ đáp ứng khoảng 10% tính năng cần thiết, còn công việc đường ống hơn cả mã nghiệp vụ
- Việc chuyển đổi dần dần đòi hỏi phải tự ghép nối routing, truy cập dữ liệu, phân quyền, theo dõi thay đổi, tìm kiếm, tác vụ nền, pub/sub, email, OAuth 2.0, SMTP/POP, object storage và tạo PDF, nên gần như không làm tăng giá trị cho người dùng
- Cuối cùng quyết định đưa phần đã chuyển sang stack mới quay lại Rails, và quyết định này bị ảnh hưởng bởi việc một mình duy trì ứng dụng Rails, căng thẳng vì không đáp ứng được nhu cầu người dùng, cùng độ phức tạp khi phải sửa đồng thời ở nhiều nơi
- Sau khi quay lại Rails, tốc độ phát triển tính năng, cải thiện tính năng cũ, cập nhật UI và sửa lỗi đã tăng trở lại; các nhược điểm của Ruby/Rails được xử lý bằng kiểm thử, refactor, Turbo và caching để tập trung vào phát triển sản phẩm
Điểm khởi đầu: chọn Rails như một phương án tạm thời
- Năm 2022, khi bắt đầu xây dựng ứng dụng lập tờ khai hải quan, tác giả đã xem xét nhiều ngôn ngữ và framework như .NET/F#, Go, Rust, React và thực sự viết thử mã với chúng
- Lựa chọn cuối cùng là Ruby on Rails
- Đây là công cụ đã quen thuộc, và cảm nhận là có thể tiến nhanh hơn gấp 10 lần so với các lựa chọn phải học mới
- Khi đó, bản triển khai bằng Rails gần giống một proof of concept tạm thời, và về sau có kế hoạch viết lại bằng một ngôn ngữ khác không “dễ mắc sai lầm” như Ruby
- Ứng dụng Rails nhanh chóng bổ sung được nhiều tính năng, và đồng nghiệp cũng thích kết quả tạo ra
- Có thể nhanh chóng xây dựng các tính năng giúp tăng năng suất
- Ban đầu còn có thể dùng mã để chặn các tình huống lỗi xảy ra thường xuyên
Điểm nghẽn của “version 2” với Rust và SvelteKit
- “version 2” chọn Rust cho backend và SvelteKit cho frontend
- Bản đầu tiên trông đẹp và chạy nhanh, nhưng chỉ có khoảng 10% số tính năng cần thiết
- Rust không có framework tích hợp tương đương với Rails, nên phải viết nhiều mã nền tảng và công việc kết nối hơn là logic nghiệp vụ
- Dần trở nên rõ ràng rằng để hoàn thiện version 2 thì phải dừng hoàn toàn việc phát triển version 1, và cuối cùng version 2 bị loại bỏ
- Việc viết lại được xác nhận là có thể mất thời gian gần tương đương với lúc xây dựng ứng dụng ban đầu
Độ phức tạp vận hành do chuyển đổi từng bước
- Mong muốn đưa Rust vào ứng dụng vẫn còn đó vì kiểu tĩnh mạnh, hiệu năng nhanh và sự yên tâm khi biên dịch thành công
- Dù đã dùng Ruby hơn 4 năm, các vấn đề runtime vẫn tiếp tục xảy ra
- Chủ yếu là giá trị null và các ngoại lệ không được xử lý
- Sau khi triển khai, tác giả theo dõi công cụ báo lỗi để nhanh chóng sửa sự cố production
- Ở frontend Rails, điều gây khó chịu là logic và markup bị phân tán qua nhiều tệp, trong khi Svelte giúp gom logic liên quan vào một tệp dễ hơn
- Kế hoạch là từ từ viết frontend mới bằng SvelteKit và backend mới bằng Rust, rồi dần thay thế cả phần Rails hiện có để cuối cùng loại bỏ Rails
- Ban đầu, việc chuyển các trang “index” có danh sách bản ghi và bộ lọc sang stack mới diễn ra khá nhanh
- Stack mới chạy nhanh và phần Rust hầu như không có vấn đề runtime
- Các quy tắc triển khai để quyết định route nào đi tới Rails, Rust hay SvelteKit trở nên phức tạp
- Độ phức tạp này được chấp nhận như trạng thái tạm thời cho tới khi chuyển đổi hoàn toàn
Công việc kỹ thuật lớn hơn giá trị người dùng
- Theo thời gian, phía Rails tiếp tục được thêm các tính năng mới hữu ích, còn các tính năng hiện có cũng ngày càng sâu và tinh vi hơn
- Đã có nhiều đợt refactor để sửa các giả định sai ban đầu, và tác giả kết luận rằng một schema cơ sở dữ liệu được thiết kế tốt sẽ có ích cho bất kỳ phiên bản nào
- UI Rails được xem là sắp bị thay thế nên quyết định không đầu tư thêm nhiều thời gian vào đó
- Vài tháng sau, việc kinh doanh thành công đủ để có thể tuyển lập trình viên toàn thời gian, và tác giả kỳ vọng quá trình chuyển sang Rust sẽ tăng tốc
- Trên thực tế, rất nhiều thời gian trong stack mới bị dùng cho việc viết các tầng trừu tượng và cơ chế
- truy cập cơ sở dữ liệu
- xử lý phân quyền để người dùng chỉ nhìn thấy dữ liệu họ được phép xem
- theo dõi mọi thay đổi
- tìm kiếm và lọc
- ép buộc ràng buộc thông qua hệ thống kiểu
- Sau đó danh sách tính năng cần thiết còn tiếp tục dài ra
- tác vụ nền
- hệ thống pub/sub để cập nhật client
- chức năng email
- OAuth 2.0 để kết nối dịch vụ bên ngoài
- SMTP và POP
- object storage
- tạo PDF
- Thư viện Rust thì đủ dùng, nhưng công việc chọn thư viện phù hợp và kết nối tất cả lại với nhau là rất lớn
- Các cuộc thảo luận về chi tiết triển khai kỹ thuật như chọn Rust ORM hay thời điểm phát hành và điểm cải tiến của Svelte 5 bắt đầu nhiều lên
- Sau khoảng 1 năm viết Rust và Svelte, trở nên rõ ràng rằng người dùng hầu như không thu được lợi ích nào từ công việc này
- Một số màn hình của stack mới mượt và nhanh, nhưng đó không phải những chỗ vốn là vấn đề
- Những trang được dùng nhiều nhất vẫn chạy trên Rails, và có số tính năng nhiều hơn gấp 10 lần “bản beta” của stack mới
- Có những khoảng thời gian vài ngày hoặc vài tuần trôi qua mà không thêm được giá trị trực tiếp nào
Quyết định quay lại Rails
- Cuối cùng quyết định viết lại bằng Rails cho những phần đang chạy trên stack mới
- Đây không phải là viết lại toàn bộ ứng dụng, mà là đưa những phần đã chuyển sang Rust/SvelteKit quay về Rails
- Quyết định này khó khăn nhưng hợp lý
- Đã đầu tư rất nhiều thời gian và mã nguồn
- Có khả năng các thành viên trong nhóm không thích Ruby hay Rails
- May mắn là cả nhóm hiểu quyết định này và sẵn sàng học cái mới
- Điểm mạnh của Rails là nhiều tính năng có sẵn ngay từ đầu, hệ sinh thái thư viện trưởng thành và ổn định, cùng thiết kế giàu quy ước và quan điểm giúp mã ngắn gọn
- Rails giúp tập trung vào việc làm sản phẩm và giảm gánh nặng phải tự viết mã hỗ trợ bên dưới
Những bài học còn lại sau khi trở về Rails
- Nỗ lực với Rust và Svelte không hoàn toàn là lãng phí
- Việc học ngôn ngữ và framework mới mang lại những góc nhìn và ý tưởng có ích cả ở các ngôn ngữ khác
- Viết ứng dụng web là chuỗi liên tục của lựa chọn và đánh đổi
- Hoặc chấp nhận khiếm khuyết của framework, hoặc tự đầu tư triển khai, và cần cân nhắc xem khoản đầu tư đó có tạo lợi thế cạnh tranh và phần thưởng dài hạn hay không
- Việc đưa mọi thứ trở lại Rails phần lớn khá đơn giản và cũng làm giảm căng thẳng
- Trong vài tuần gần đây, tác giả lại cảm nhận được năng suất trong phát triển tính năng mới, cải thiện tính năng cũ, cập nhật UI và sửa lỗi
- Các nhược điểm của Ruby và Rails được chấp nhận như những điều kiện cần tính đến khi viết mã
- Nếu có nhiều vấn đề runtime thì cần kiểm thử nhiều hơn
- Mã spaghetti khó bảo trì thường dễ do lập trình viên tạo ra hơn là do ngôn ngữ hay framework
- Cần dành nhiều thời gian hơn cho refactor và tổ chức mã
- Nếu mở rộng Rails đã khó thì mở rộng thứ khác cũng khó
- Rails đủ nhanh, và có thể lách qua bằng Turbo cùng caching
- Từ góc nhìn người dùng, stack Rust không nhanh hơn rõ rệt
- Trong Rails, P95 duration của trang nặng nhất là 338ms, vẫn còn chỗ để cải thiện nhưng đã đủ nhanh để tạo cảm giác ổn
- Không có ngôn ngữ hay framework nào hoàn hảo; điều cần thiết là hiểu rõ điểm mạnh, điểm yếu và cách xử lý chúng
- Rails là một trong những framework rất tốt cho người muốn tập trung làm ra sản phẩm, và những framework lâu đời khác với cộng đồng đóng góp năng động cũng có thể là lựa chọn tốt tương tự
2 bình luận
Viết lại bằng Ruby
Viết lại bằng Rails
Ý kiến trên Hacker News
Tôi cho rằng Rails vượt xa các stack khác khi xây dựng ứng dụng web, kể cả Django hay Laravel
Nó đã xử lý hết những phần nhàm chán, hiểu rõ các yêu cầu cần thiết và ít cản trở hơn khi làm ứng dụng thực tế. Sau 15 năm phát triển ứng dụng web, tôi đã dùng Meteor, Remix, Next.js, Node.js dựa trên Express và nhiều thứ khác, nhưng tôi thấy phát triển web về cơ bản đã là một bài toán được giải khá nhiều rồi. Phía JavaScript giống như các lập trình viên bị dòng tiền của các tập đoàn lớn đẩy sang việc nghịch đồ chơi mới, còn Rust hay Go thì tốt cho phát triển hệ thống và hạ tầng triển khai, nhưng nhét ép vào phát triển web thì không hợp lý
Nó tích hợp dễ dàng với vô số thư viện Java và là ngôn ngữ kiểu tĩnh nên hữu ích trong nhiều tình huống. Chỉ riêng việc binding dữ liệu theo quy ước thông qua Jackson cũng đã rất xuất sắc từ rất lâu rồi, trong khi ở các ngôn ngữ kiểu động, cảm giác những phần như vậy thường được vá thêm về sau
Nhưng sau khi đã đi sâu vào Rails, Django hay Laravel mà cần làm việc gì đó lệch khỏi quy ước thì có thể trở nên khó nhằn. Vì không tự tay viết glue code nên nhiều khi thứ xuất hiện là khoảng trống trong hiểu biết hơn là rào cản kỹ thuật. Dù vậy, nếu thật sự học kỹ bên trong thì chỉ riêng việc đọc code cũng có thể tiết kiệm rất nhiều thời gian lập trình. Tất nhiên trên thực tế người ta lại không làm vậy, vì viết lại từ đầu quá vui nên dù không hợp lý về mặt kinh doanh vẫn thích thử cái mới
Đừng chỉ tin ý kiến của tôi, hãy xem dữ liệu: https://www.youtube.com/watch?v=Qp9SOOtgmS4
Nếu dùng Rails cho một dịch vụ mà bạn không nắm chắc số người dùng hoặc số request mỗi giây, bạn có thể tiết kiệm chi phí ở thời điểm phát hành nhưng lại tốn thêm chi phí máy chủ
Bản demo của Rails không hấp dẫn đến vậy, ngoài việc cho thấy vận may quan trọng thế nào trong chuyện được chú ý và được chấp nhận. Đến giờ tôi vẫn không thấy rõ giá trị của nó và sẽ chọn Spring hoặc ASP.NET. Những nhà sáng lập của startup nền Tcl đó sau này đã tạo ra OutSystems, một trong số ít công cụ RAD thành công ở quy mô phát triển web doanh nghiệp và cũng là một câu chuyện thành công của ngành CNTT Bồ Đào Nha
Trước đây tôi thích Rails hơn, nhưng trong hơn 10 năm qua Python đã phát triển mạnh nên Django trở thành lựa chọn thực tế hơn. Các lập trình viên machine learning và data science vốn đã quen với Python, còn tài liệu của Django thì quá tốt nên khi cần họ có thể đạt năng suất chỉ trong thời gian rất ngắn. Tôi đã tận mắt thấy điều đó ở nhiều dự án
Django cũng giảm bớt các quyết định và tranh cãi về cấu trúc ứng dụng so với các thư viện kém áp đặt hơn hoặc các microframework. Cộng đồng Django/FLOSS cũng năng động hơn tôi tưởng rất nhiều, và việc tương tác với họ cũng rất dễ chịu. Chỉ là tôi ước nó có các generator kiểu Rails và tính năng seed dữ liệu tích hợp sẵn, ví dụ như
rake db:seedTôi đã dùng Rails 17 năm rồi, vẫn rất thích và còn ưu tiên nó cho cả frontend
Nó cho bạn mọi lựa chọn để tách code mà không biến phần đầu file thành một danh sách
includesdài lê thê không cần thiết như trong các ứng dụng Angular. Tôi không thấy có nhược điểm nào trong việc tách logic và markup ở frontend. Có thể test logic một cách độc lập, và khi tìm bug thường biết được đó là vấn đề ở markup hay ở logic nên việc lần vết dễ hơn. Tôi cũng từng viết khá nhiều method Ruby dài hơn 200 dòng, nhưng đó hoàn toàn là lỗi của tôi, và nếu có hệ thống unit test chi tiết hơn thì đã ngăn được điều đóincludesdài là biến mọi thứ thành toàn cụcTôi không chắc đó có phải là ưu điểm hay không. Import có thể chiếm khá nhiều chỗ, nhưng nó cho thấy một cách tường minh mọi thứ được kết nối với nhau như thế nào. Nếu thấy vướng mắt thì cứ gấp chúng lại trong IDE
Tôi thích bài viết này vì nó thực tế. Câu cốt lõi này đáng để luôn ghi nhớ
“Điều thú vị là những thứ từng làm tôi bực mình ở Ruby và Rails giờ đây đỡ khó chịu hơn nhiều. Tôi đã chấp nhận rằng không có ngôn ngữ hay framework nào là hoàn hảo. Chỉ cần biết điểm mạnh và điểm yếu của chúng rồi xử lý thôi”
Trong nhiều trường hợp họ chưa hiểu framework đủ sâu, và cũng không biết vì sao nó lại được thiết kế như vậy. Nhưng biết rõ giới hạn của nó và làm việc cùng framework hiệu quả hơn rất nhiều. Bạn nên dành thời gian để thực sự hiểu công cụ mình đang dùng
“Trong thời gian qua, tôi đã bắt đầu xây dựng ‘phiên bản 2’ của ứng dụng. Tôi chọn Rust cho backend và SvelteKit cho frontend.”
Vì v1 bắt đầu với Rails, tôi đọc nó như một lời nhắc hãy cẩn thận khi từ bỏ một framework vốn đã hoạt động tốt
Rust là một ngôn ngữ do những người rất thông minh tạo ra, nhưng có cảm giác trọng tâm của nó nghiêng nhiều hơn về lý thuyết kiểu và tiết kiệm thêm vài lần cấp phát, thay vì giúp một lập trình viên thực chiến như tôi tạo ra thứ gì đó hữu ích và nhất quán
Dù đã dùng Rust từ năm 2020, mỗi khi đọc các bài viết blog về Rust tôi vẫn theo nghĩa đen là không hiểu được một nửa
Đặc biệt là khi so với các ngôn ngữ chủ lưu khổng lồ. Ví dụ, trait bao trùm cả trường hợp sử dụng của abstract class và concept trong C++, cũng như abstract class và protocol trong Python. Cũng không có sự thiếu nhất quán kiểu nên dùng exception hay trả về giá trị để xử lý lỗi. Có panic, nhưng nó được dùng giống như assert. Khi
if-elselà một biểu thức thì cũng không cần toán tử ba ngôi riêngGần đây có xu hướng muốn dùng Rust cho mọi thứ, nhưng đây không phải là ngôn ngữ được tạo ra để dùng cho ứng dụng CRUD. Suy cho cùng, bài viết này gần như là một nghiên cứu tình huống cho câu “hãy dùng công cụ phù hợp với công việc. Ngôn ngữ bạn thích trong thời gian rảnh hoặc ngôn ngữ đang thịnh hành chưa chắc đã phù hợp”
Nếu bạn chấp nhận được garbage collector thì không cần dùng Rust hay C++ để phải bận tâm đến quản lý bộ nhớ. Có người quảng bá Rust như một ngôn ngữ lập trình đa dụng, và về mặt lý thuyết điều đó là đúng, nhưng C++ những năm 90 cũng từng được quảng bá như vậy. Việc tối thiểu hóa cấp phát bộ nhớ thường chính là đặc tính mà người ta mong muốn khi chọn Rust hoặc C++
Rất khó để viết nhanh một ứng dụng web lớn bằng Rust
Một ứng dụng lớn viết bằng Rails có thể không chạy nhanh, có thể nhiều lỗi, hoặc khó bảo trì và mở rộng. Các lựa chọn như Java, C#, Go có thể không sexy hay trông ngầu, nhưng vẫn có thể hoàn thành công việc nhanh gần như Rails trong khi chạy với tốc độ gần Rust. Thông thường chúng cũng ít lỗi hơn, và dự án sẽ dễ bảo trì cũng như mở rộng hơn
Dù bạn có ghét Rails đến đâu đi nữa, điều này cũng giống như nói rằng có thể làm blog nhanh hơn WordPress bằng C++
Nếu được xây dựng cẩn thận thì không nhất thiết phải như vậy. Tôi đã thấy nhiều ứng dụng Rails lớn, gần như không có lỗi, ổn định và dễ chỉnh sửa, và cũng đã thấy cả ứng dụng Rails lẫn Go rất tệ. Một số ngôn ngữ hay framework có thể khiến người ta dễ rơi vào tình trạng đó hơn, nhưng cái gì cũng có khả năng như vậy, đồng thời luôn có những đánh đổi khiến nó khó ở những khía cạnh khác. Nếu có kỷ luật thì cái gì cũng có thể vận hành tốt
Với các lập trình viên có trình độ tương đương, Rails trong phần lớn tính năng ứng dụng web thường được phát triển nhanh hơn Java, C#, Go. Theo kinh nghiệm của tôi thì có khi nhanh hơn tới 3 lần. Tôi đã làm cả với monolith lớn lẫn kiến trúc hướng dịch vụ, và cũng từng làm cùng các nhóm Java. Có thời điểm tôi còn đưa Go vào một tổ chức Rails vì từng tin điều tương tự
Cuối cùng, vấn đề thực sự là MVC không phù hợp với mọi thứ, và chúng tôi đã không dùng namespace đủ chặt chẽ. Nếu khắc phục điều đó trong những phạm vi nhỏ và có mục tiêu, Rails vẫn hoàn toàn ổn
Vì vậy, tôi cho rằng với đủ mức độ quản lý, kiến trúc tốt và bảo trì phù hợp thì Rails vẫn làm được
Gần đây tôi bắt đầu thử AdonisJS như một lựa chọn thay thế TypeScript cho Rails
Nếu bạn quan tâm đến “Rails viết bằng TypeScript” thì nên thử một lần. Nó cho cảm giác gần với Rails hoặc Laravel hơn rất nhiều so với các stack TypeScript phổ biến nhất, và theo kiểu kèm sẵn đầy đủ thay vì bắt bạn phải tự chọn mọi phần của stack hoặc tin vào template bên thứ ba
Gần đây tôi bị thu hút bởi các backend framework kiểu kèm sẵn đầy đủ như Rails, Laravel, Django, nhưng vì gần như suốt sự nghiệp tôi đều dùng TypeScript nên cứ tiếp tục như hiện tại có lẽ dễ hơn là chuyển sang thứ khác. Tôi tò mò không biết cảm nhận của bạn sau khi thử ra sao, và liệu có thể so sánh nó với những framework khác mà bạn đã nhắc tới không
Gần đây trong các dự án cá nhân tôi đã chọn NestJS, nhưng rồi sớm biết đến Adonis và đang cân nhắc có nên chuyển hẳn sang không
Lý do các công nghệ như Next.js và SvelteKit phổ biến trong giới lập trình viên JavaScript là vì chúng giúp giảm bớt vấn đề trải nghiệm phát triển backend trên Node.js không tốt
Xây backend bằng TypeScript khá giống xây bằng C#, nhưng lại có cảm giác thiếu đi những ưu điểm của một ngôn ngữ được thiết kế bài bản. Nó rất khác với trải nghiệm làm backend bằng Rails. Các lập trình viên Node.js cũng đang bắt đầu nhận ra rằng vấn đề không chỉ nằm ở hệ sinh thái và công cụ, mà framework cung cấp một lời giải tổng thể cho việc xây dựng ứng dụng mới là điều quan trọng. Phần mà Next.js và SvelteKit hỗ trợ chính là chỗ đó, và đó cũng là điều Rails đã cách mạng hóa từ năm 2006
Ý là chỉ vì nó có thể chạy code ở backend rồi đem ra so với Rails sao? Hỗ trợ truy cập cơ sở dữ liệu, migration, ORM, queue, tác vụ định kỳ, validation, i18n, authentication, authorization ở đâu? Thứ duy nhất Next.js giúp là đẩy mọi người vào nền tảng Vercel
Còn với frontend thì tôi thích TypeScript hơn JavaScript
Mình hơi bối rối trước những bình luận cho rằng Rust không hợp lắm với web backend
Khi chủ yếu làm các dịch vụ nhỏ như beampaint.com hay probablyup.net, mình lại có trải nghiệm ngược lại. Có thể khi xây dựng ứng dụng lớn hơn thì độ phức tạp sẽ tăng mạnh, và có lẽ chắc là vậy, nhưng Rust rất giỏi trong việc mang lại sự tự tin cần thiết cho các codebase lớn
Các framework lâu đời có sẵn những thứ đó như mặc định, còn framework Rust thì do tuổi đời nên khả năng có đủ là thấp hơn. Cứ tưởng tượng số tính năng còn thiếu tăng từ hai lên sáu là được
Bài này gần như là câu chuyện đời mình
Mình từng dùng Rails rồi đến năm 2013 ra mắt một ứng dụng SaaS viết bằng Go, và phải tự mày mò mọi thứ. Migration, tầng cơ sở dữ liệu, asset pipeline, triển khai, kiểm thử hợp lệ, ghi log, thanh toán... Rails có quá nhiều thứ được cung cấp sẵn
Mình không thích Go, nhưng ở đây thứ đáng bị trách không phải là Go. Dùng Go cho ứng dụng CRUD ngay từ đầu đã là một quyết định khủng khiếp
Django cũng cho cảm giác tương tự
Các vấn đề hiệu năng thường phát sinh do truy vấn cơ sở dữ liệu hơn là từ chính Django
Những tối ưu đơn giản nhưng hay bị quên gồm có: tạo chỉ mục cơ sở dữ liệu, đẩy nhiều logic sang cơ sở dữ liệu hơn thay vì để trong code, ví dụ dùng pipeline của MongoDB hoặc partial index của Postgres, bật nén Brotli/zstd cho hầu hết request, cache object hoặc request bằng bộ nhớ đệm như Redis, và đặt các header kiểm soát cache phù hợp. Ở frontend cũng cần các tối ưu như dùng canvas thay vì render hàng nghìn đối tượng thành DOM node. Gần đây mình đã thấy vấn đề này trong một ứng dụng bản đồ