Đây là một trong những bài viết về nội dung Hướng dẫn tối ưu để áp dụng thành công Kotlin trong môi trường chủ yếu dùng Java, một series theo dõi quá trình Kotlin được đội ngũ thực tế áp dụng, từ sự tò mò của một lập trình viên cho đến sự chuyển đổi cho toàn bộ công ty.
Giai đoạn Đánh giá: Kotlin không còn là một sân chơi thử
Khi bạn đã quen với Kotlin trong các bài kiểm tra thử, đã đến lúc thực hiện đánh giá kỹ hơn. Bạn có hai cách tiếp cận chính:
- Xây dựng một microservice hoặc ứng dụng vi mô mới bằng Kotlin.
- Mở rộng/chuyển đổi một ứng dụng Java hiện có.
1.Xây dựng một microservice/ứng dụng mới bằng Kotlin
Việc bắt đầu lại từ đầu với một ứng dụng hoặc microservice mới sẽ mang lại trải nghiệm Kotlin trọn vẹn mà không bị hạn chế bởi mã nguồn cũ. Cách tiếp cận này thường mang lại trải nghiệm học tốt nhất và thể hiện rõ nhất những điểm mạnh của Kotlin.
Pro tip: Hãy tìm sự hỗ trợ từ chuyên gia ở giai đoạn này. Dù các lập trình viên thường rất tự tin vào khả năng của mình, tránh được những sai lầm ban đầu như viết Kotlin theo phong cách Java hoặc thiếu thư viện tận dụng sức mạnh của Kotlin có thể giúp bạn tiết kiệm nhiều tháng nợ kỹ thuật.
Đây là cách bạn có thể tránh các bẫy thường gặp khi sử dụng Kotlin xuất phát từ nền tảng Java.
Pitfall: Chọn một framework khác với framework bạn đang dùng trong Java.
Tip: Hãy gắn bó với framework hiện có của bạn.
Nhiều khả năng bạn đã sử dụng Spring Boot với Java, vậy nên hãy sử dụng nó với Kotlin. Spring Boot hỗ trợ Kotlin ở mức độ cao nhất, vì vậy không có lợi ích nào khi chuyển sang framework khác. Hơn nữa, bạn sẽ phải học không chỉ một ngôn ngữ mới mà còn cả một framework mới, điều này chỉ làm tăng thêm độ phức tạp mà không mang lại bất kỳ lợi ích nào.
Important: Spring can thiệp vào nguyên tắc “kế thừa theo thiết kế” của Kotlin, vốn yêu cầu bạn phải đánh dấu rõ ràng các lớp là là open để có thể kế thừa chúng.
Để tránh việc thêm từ khóa open vào tất cả các lớp liên quan đến Spring (như Để tránh việc thêm từ khóa open vào tất cả các lớp liên quan đến Spring (như @Configuration,…) hãy sử dụng plugin build sau: https://kotlinlang.org/docs/all-open-plugin.html#spring-support. Nếu bạn tạo một dự án Spring bằng công cụ Spring Initializr trực tuyến nổi tiếng, plugin build này đã được cấu hình sẵn cho bạn.
Pitfall: Viết Kotlin theo kiểu Java, dựa vào các API phổ biến của Java thay vì thư viện chuẩn của Kotlin.
Danh sách này có thể rất dài, vì vậy hãy tập trung vào những cạm bẫy phổ biến nhất.
Pitfall 1: Sử dụng Java Stream thay vì Kotlin Collection
Tip: Luôn sử dụng Kotlin Collection
Kotlin Collections hoàn toàn tương thích với Java Collections, nhưng khi đi kèm với các hàm bậc cao (higher – order functions) trực quan và đầy đủ tính năng, khiến Java Stream trở nên lỗi thời.
Dưới đây là một ví dụ nhằm mục đích chọn ra 3 sản phẩm hàng đầu theo doanh thu (giá * số lượng bán ra) theo từng nhóm danh mục sản phẩm.
Java

Kotlin

Tính tương tác giữa Kotlin Java cho phép bạn làm việc với các lớp và record của Java như thể chúng là Kotlin gốc, mặc dù bạn cũng thể sử dụng một lớp (data class) Kotlin thay thế.
Pitfall 2: Tiếp tục sử dụng Optional của Java
Tip: Hãy sử dụng kiểu dữ liệu Nullable
Một trong những lý do chính khiến các lập trình viên Java chuyển sang Kotlin là vì tính hỗ trợ nullable tích hợp sẵn của Kotlin, giúp tạm biệt với NullPointerExceptions. Vì vậy, hãy thử chỉ sử dụng các kiểu Nullbable, không dùng thêm Optionals nữa. Bạn vẫn còn Optionals trong giao diện của mình không? Đây là cách bạn dễ dàng loại bỏ chúng bằng cách chuyển đổi thành kiểu Nullable:
Kotlin

Pitfall 3: Tiếp tục sử dụng các lớp wrapper tĩnh (static wrappers)
Tip: Hãy sử dụng các phương thức mở rộng (Extension methods).
Phương thức mở rộng (Extension methods) mang lại nhiều lợi ích:
- Chúng giúp code của bạn mượt mà và dễ đọc hơn nhiều so với các lớp wrapper.
- Chúng ta có thể được tìm thấy thông qua tính năng code completion, điều mà các wrapper không làm được.
- Vì các Extension cần được import, chúng cho phép bạn lựa chọn sử dụng chức năng mở rộng trong một phần cụ thể của ứng dụng.
Java

Kotlin

Hãy lưu ý rằng Kotlin cung cấp các phương thức và biến cấp cao nhất. Điều này có nghĩa là chúng ta có thể dễ dàng khai bao, ví dụ DEFAULT_DATE_TIME_FORMATTER ở cấp cao nhất mà không cần phải gắn vào một đối tượng như trong Java.
Pitfall 4: Dựa vào các API Java một cách vụng về.
Tip: Hãy sử dụng đối tác tiện lợi của Kotlin.
Thư viện chuẩn Kotlin sử dụng các phương thức mở rộng để làm cho các thư viện Java thân thiện hơn với người dùng, mặc dù nền tảng triển khai vẫn là Java. Hầu hết các thư viện và framework của bên thứ ba lớn hơn, chẳng hạn như Spring, cũng đã làm điều tương tự.
Ví dụ về thư viện chuẩn:
Java

Kotlin

Example Spring:
Java

Kotlin

Pitfall 5: Sử dụng một file riêng cho mỗi lớp public
Tip: Kết hợp các lớp public liên quan vào cùng một file.
Điều này giúp bạn dễ dàng nắm bắt cách một (sub-)domain được cấu trúc mà không cần phải điều hướng qua hàng chục file.
Java

Kotlin

Pitfall 6: Dựa vào mô hình lập trình mutable
Tip: Hãy ưu tiên immutability – đây là mặc định trong Kotlin.
Xu hướng trong nhiều ngôn ngữ lập trình – bao gồm cả Java – là rõ ràng: immutability đang thắng thế so với mutability.
Lý do rất rõ ràng: bất biến ngăn ngừa các các tác động phụ không mong muốn, khiến code an toàn hơn, dễ dự đoán hơn và dễ lý giải hơn. Nó cũng đơn giản hóa lập trình đồng thời, vì dữ liệu bất biến có thể được chia sẻ tự do giữa các luồng mà không có nguy cơ xảy ra race condition.
Đó là lý do tại sao hầu hết các ngôn ngữ hiện đại – trong đó có Kotlin – đều nhấn mạnh tính bất biến theo mặc định hoặc khuyến khích mạnh mẽ tính bất biến. Trong Kotlin, tính bất biến là mặc định, mặc dù khả năng thay đổi vẫn có thể được sử dụng khi thật sự cần thiết.
Dưới đây là hướng dẫn nhanh về gói tính năng bất biến của Kotlin:
1. Sử dụng val thay vì var
Ưu tiên val thay vì var. IntelliJ IDEA sẽ thông báo cho bạn nếu bạn sử dụng var, mà val có thể được sử dụng.
2. Sử dụng các data class với copy (…)
Đối với các lớp liên quan đến domain, hãy sử dụng data class với val. Data class trong Kotlin thường được so sánh với Java records. Mặc dù có một số điểm trùng lặp, nhưng data class có tính năng nổi bật là copy (…), mà việc thiếu tính năng này khiến việc chuyển đổi record – điều thường cần thiết trong logic nghiệp vụ – trở nên rất rườm rà.
Java

Kotlin

Hơn nữa, hãy sử dụng data classes cho những lớp liên quan đến domain bất cứ khi nào có thể. Tính bất biến của chúng đảm bảo trải nghiệm an toàn, ngắn gọn và dễ dàng khi làm việc với lõi ứng dụng.
Tip: Ưu tiên sử dụng Collection Immutable thay vì Collection Mutable
Các Collection bất biến mang lại những lợi ích rõ ràng về tính an toàn trong môi trường đa luồng, có thể được truyền an toàn trong môi trường đa luồng, có thể được truyền đi an toàn và dễ hiểu hơn khi phân tích. Mặc dù các Collection trong Java có cung cấp một số tính năng bất biến, việc sử dụng chúng lại khá nguy hiểm vì rất dễ gây ra ngoại lệ trong thời gian chạy:
Java

Kotlin

Điều tương tự cũng áp dụng khi sử dụng Collections.unmodifiableList(…) vốn không chỉ không an toàn mà còn đòi hỏi thêm việc cấp phát bộ nhớ.
Java

Kotlin

Khi nói đến xử lý đồng thời, các cấu trúc dữ liệu bất biến, bao gồm cả các collection, nên được ưu tiên sử dụng. Trong Java, bạn phải tốn nhiều công sức với các Collection đặc biệt có API hoặc giới hạn, như CopyOnWriteArrayList. Ngược lại, trong Kotlin, List<…> chỉ-đọc (read-only) là đủ đáp ứng hầu hết các trường hợp sử dụng.
Nếu bạn cần các Collection mutable và an toàn trong môi trường đa luồng (thread-safe), Kotlin cung cấp Persistent Collections (persistentListOf(…), persistentMapOf(…)), tất cả đều dùng chung một giao diện mạnh mẽ.
Java

Kotlin

Pitfall 7: Tiếp tục sử dụng builder (hoặc tệ hơn: cố gắng dùng Lombok)
Tip: Hãy sử dụng named arguments
Builder rất phổ biến trong Java. Mặc dù chúng tiện lợi, nhưng chúng tạo thêm mã thừa, không an toàn và làm tăng độ phức tạp. Trong Kotlin, chúng trở nên vô dụng vì một tính năng ngôn ngữ đơn giản khiến chúng lỗi thời: named arguments.
Java

Kotlin

2. Mở rộng/chuyển đổi một ứng dụng Java hiện có
Nếu bạn không có cơ hội bắt đầu mới để dùng thử Kotlin, thì việc thêm các tính năng Kotlin mới hoặc toàn bộ các mô – đun Kotlin vào một mã nguồn Java hiện có là cách phù hợp nhất. Nhờ khả năng tương tác liền mạch giữa Kotlin và Java, bạn có thể viết mã Kotlin sao cho trông như Java đối với các đoạn mã gọi từ Java. Cách tiếp cận này cho phép:
- Di chuyển dần mà không cần viết lại toàn bộ
- Kiểm thử Kotlin trong môi trường thực tế của bạn
- Xây dựng sự tự tin của đội ngũ thông qua mã Kotlin trong môi trường sản xuất
Thay vì bắt đầu từ một điểm cụ thể hãy xem xét các phương pháp tiếp cận khác nhau sau:
Outside-in:
Bắt đầu từ phần “leaf” của ứng dụng, ví dụ như trong controller, batch job,… rồi sau đó tiến dần về phía lõi của domain. Cách làm này sẽ mang lại những lợi ích sau:
- Compile-time isolation: Các lớp leaf hiếm khi phụ thuộc vào bất kỳ thành phần nào khác, vì vậy bạn có thể chuyển sang Kotlin mà vẫn có thể xây dựng phần còn lại của hệ thống mà không thay đổi gì.
- Fewer ripple edits. Một UI/controller đã được chuyển đổi có thể gọi mã domain Java hiện có gần như không cần thay đổi nhờ khả năng tương tác liền mạch.
- Các bản cập nhật nhỏ hơn, quá trình xem xét dễ dàng hơn. Bạn có thể di chuyển từng tệp hoặc từng tính năng một.
Inside-out:
Bắt đầu từ phần lõi rồi sau đó tiến ra các lớp bên ngoài thường là một phương pháp rủi ro hơn, vì nó làm giảm các ưu điểm của phương pháp outside-in đã đề cập ở trên. Tuy nhiên, đây vẫn là một lựa chọn khả thi trong các trường hợp sau:
- Phần lõi rất nhỏ hoặc độc lập. Nếu lớp domain của bạn chỉ bao gồm một vài POJO và dịch vụ, việc chuyển đổi sớm có thể tốn ít công sức và ngay lập tức khai thác được các cấu trúc lập trình tiêu chuẩn của Kotlin (data class, value class, sealed hierarchies).
- Dù sao thì cũng phải tái cấu trúc. Nếu bạn có dự định refactor các invariant hoặc đưa vào các pattern DDD (value object, aggregate) trong quá trình di chuyển, đôi khi việc thiết kế lại domain bằng Kotlin trước sẽ gọn gàng hơn.
- Các ràng buộc null-safety nghiêm ngặt. Đưa Kotlin vào trung tâm sẽ biến domain thành một “pháo đài an toàn với null”; các lớp Java bên ngoài vẫn có thể gửi giá trị null, nhưng các ranh giới trở nên rõ ràng và kiểm soát hơn.
Module by module
Nếu kiến trúc của bạn được tổ chức theo chức năng thay vì theo lớp, và module có kích thước dễ quản lý, việc chuyển đổi từng module một là một chiến lược tốt.
Các tính năng ngôn ngữ để chuyển đổi Java sang Kotlin
Kotlin cung cấp nhiều tính năng – chủ yếu là các chú thích – cho phép mã Kotlin của bạn hoạt động giống như Java thuần. Điều này đặc biệt hữu ích trong các môi trường tương lai, nơi Kotlin và Java cùng tồn tại trong một mã nguồn.
Kotlin

Java

Kotlin

Java

Trình chuyển đổi Java sang Kotlin của IntelliJ IDEA
IntelliJ IDEA cung cấp một công cụ chuyển đổi Java sang Kotlin, vì vậy về mặt lý thuyết, công cụ này có thể thực hiện việc chuyển đổi cho bạn. Tuy nhiên, mã nguồn kết quả còn xa mới hoàn hảo, vì vậy hãy sử dụng nó như điểm khởi đầu. Từ đó, bạn có thể chuyển sang một dạng phù hợp hơn với Kotlin. Chủ đề này sẽ được thảo luận chi tiết trong phần cuối của loạt bài viết này: các yếu tố thành công cho việc áp dụng Kotlin ở quy mô lớn.
Lấy Java làm điểm xuất phát có thể khiến bạn viết Kotlin theo phong cách Java, điều này mang lại một số lợi ích nhưng sẽ không khai thác hết tiềm năng của Kotlin. Do đó, việc viết một ứng dụng mới là phương pháp mà tôi ưa chuộng.
