15 tháng 8 năm 2023 | Máy tính
Bài viết này sẽ trình bày cách học một số phương pháp viết thông dụng và thực hành tốt nhất trong Kotlin bằng cách so sánh với Java, nhằm giúp những người chuyển từ Java sang Kotlin có thêm hiểu biết.
1. Nên sử dụng biểu thức thay vì khối hàm khi có thể
Đầu tiên, hãy xem một đoạn mã Java:
public static String ageGroup(int age) {
if (age >= 0 && age < 18) {
return "Trẻ em";
} else if (age < 45) {
return "Thanh niên";
} else if (age < 60) {
return "Trung niên";
} else {
return "Cao tuổi";
}
}
Đoạn mã trên phân loại độ tuổi theo các nhóm khác nhau. Nếu viết lại đoạn mã này bằng Kotlin, chúng ta sẽ có kết quả như sau:
// Cách viết không được khuyến khích
fun ageGroup(age: Int): String {
return if (age in 0..<18) {
"Trẻ em"
} else if (age < 45) {
"Thanh niên"
} else if (age < 60) {
"Trung niên"
} else {
"Cao tuổi"
}
}
Tuy nhiên, cách viết trên không được khuyến khích. Có hai cải tiến mà bạn nên áp dụng: sử dụng biểu thức thay vì khối hàm và sử dụng when
thay vì if
.
Vì vậy, đoạn mã Kotlin đã được sửa đổi thành cách viết dưới đây:
// Cách viết được khuyến khích
fun ageGroup(age: Int): String = when {
age in 0..<18 -> "Trẻ em"
age < 45 -> "Thanh niên"
age < 60 -> "Trung niên"
else -> "Cao tuổi"
}
2. Sử dụng hàm mở rộng để đóng vai trò gói công cụ
Xem đoạn mã Java sau:
import java.text.SimpleDateFormat;
import java.util.Date;
public class DatesUtil {
public static String formatDate(Date date) {
return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(date);
}
public static void main(String[] args) {
System.out.println(formatDate(new Date()));
}
}
Đây là cách viết phổ biến của lớp công cụ tĩnh trong Java. Nếu chuyển đoạn mã này sang Kotlin, chúng ta sẽ có kết quả như sau:
// Cách viết không được khuyến khích
import java.text.SimpleDateFormat
import java.util.*
object DateUtil {
fun formatDate(date: Date): String =
SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(date)
}
fun main() {
println(DateUtil.formatDate(Date()))
}
Trong trường hợp này, cách viết từ Java chuyển sang Kotlin không được khuyến khích. Thay vào đó, Kotlin khuyến khích sử dụng hàm mở rộng để thực hiện chức năng này, làm cho mã dễ đọc hơn. Đoạn mã sử dụng hàm mở rộng như sau:
// Cách viết được khuyến khích
import java.text.SimpleDateFormat
import java.util.*
fun Date.format(): String = SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(this)
fun main() [B88bet Win](/post/threat-modeling/) {
println(Date().format())
}
3. Sử dụng tham số đặt tên thay vì chuỗi gọi Set
Xem đoạn mã Java sau:
class DatabaseConfig {
private String host;
private Integer port;
private String charset = "utf-8";
private String timezone = "Asia/Beijing";
public void setHost(String host) { this.host = host; }
public void setPost(Integer port) { this.port = port; }
public void setCharset(String charset) { this.charset = charset; }
public void setTimezone(String timezone) { this.timezone = timezone; }
}
// Sử dụng chuỗi Set để thiết lập giá trị bắt buộc
DatabaseConfig databaseConfig = new DatabaseConfig();
databaseConfig.setHost("localhost");
databaseConfig.setPost(3306);
Trong đoạn mã trên, một lớp cấu hình đã được định nghĩa, trong đó một số trường có giá trị mặc định, trong khi các trường khác cần phải được thiết lập tại thời điểm khởi tạo. Trong Kotlin, hỗ trợ sẵn tham số đặt tên và giá trị mặc định, vì vậy đoạn mã trên có thể được viết lại như sau:
data class DatabaseConfig(
val host: String,
val post: Int,
val charset: String = "utf-8",
val timezone: String = "Asia/Beijing"
)
// Sử dụng tham số đặt tên để thiết lập giá trị bắt buộc
val databaseConfig = DatabaseConfig(
host = "localhost",
post = 3306
)
Như vậy, khi khởi tạo đối tượng, chúng ta bỏ qua chuỗi gọi Set, khiến mã ngắn gọn và dễ đọc hơn.
4. Sử dụng apply để thực hiện một loạt hoạt động khởi tạo đối tượng
Xem đoạn mã Java sau:
public static void main(String[] args) {
File file = new File("test.txt");
file.setExecutable(false);
file.setReadable(true);
file.setWritable(false);
}
Đoạn mã trên là cách viết phổ biến để khởi tạo đối tượng trong Java. Chuyển đoạn mã này sang Kotlin, chúng ta sẽ có kết quả như sau:
// Cách viết không được khuyến khích
fun main() {
val file = File("test.txt")
file.setExecutable(false)
file.setReadable(true)
file.setWritable(false)
}
Trong trường hợp này, nên sử dụng hàm mở rộng apply
để thực hiện một loạt hoạt động khởi tạo đối tượng, giúp tránh phải lặp lại tên biến đối tượng trong mỗi câu lệnh.
Đoạn mã sử dụng apply
như sau:
// Cách viết được khuyến khích
fun main() {
val file = File("test.txt")
file.apply {
setExecutable(false)
setReadable(true)
setWritable(false)
}
}
5. Không nên sử dụng quá tải hàm chỉ để cung cấp giá trị mặc định
Xem đoạn mã Java sau:
public class TestOverload {
public static void main(String[] args) {
greet();
}
private static void greet() {
greet("World"); [tải game 789 club tài xỉu](/post/what-is-power-platfor/)
}
private static void greet(String name) {
System.out.println("Hello " + name);
}
}
Trong đoạn mã trên, phương thức greet
trong lớp TestOverload
được quá tải để cung cấp giá trị mặc định.
Chuyển đoạn mã này sang Kotlin, chúng ta sẽ có kết quả như sau:
// Cách viết không được khuyến khích
fun main() {
fun greet(name: String) {
println("Hello $name")
}
fun greet() {
greet("World")
}
greet()
}
Cách viết này không được khuyến khích. Trong Kotlin, bạn có thể trực tiếp sử dụng hàm với giá trị mặc định. Đoạn mã đã được sửa đổi như sau:
// Cách viết được khuyến khích
fun main() {
fun greet(name: String = "World") {
println("Hello $name")
}
greet()
}
6. Hãy tận dụng tính an toàn Null của Kotlin
Xem đoạn mã Java sau:
if (null == order || null == order.getCustomer() || null == order.getCustomer().getAddress()) {
throw new IllegalArgumentException("Invalid Order");
}
String city = order.getCustomer().getAddress().getCity();
Đoạn mã trên minh họa vấn đề kiểm tra từng tầng đối tượng lồng nhau trong Java.
Trong Kotlin, điều này không cần thiết phức tạp như vậy, chỉ cần sử dụng kiểm tra an toàn Null (?.
) và biểu thức Elvis (?:
).
Đoạn mã Kotlin đã được viết lại như sau:
// Cách viết được khuyến khích
val city = order?.customer?.address?.city ?: throw IllegalArgumentException("Invalid Order")
Ngoài ra, cần lưu ý rằng cách viết sau đây để bỏ qua kiểm tra an toàn Null và ép kiểu trực tiếp không được khuyến khích:
// Cách viết không được khuyến khích
val city = order!!.customer!!.address!!.city
7. Hãy sử dụng let
Xem đoạn mã Java sau:
Order order = getOrderById(orderId);
if (null != order) {
boolean valid = isCustomerValid(order.getCustomer());
// ...
}
Trong đoạn mã trên, trước tiên truy vấn Order, sau đó kiểm tra Order không rỗng trước khi kiểm tra tính hợp lệ của Customer bên dưới Order.
Trong Kotlin, bạn có thể sử dụng let
để thay thế kiểm tra if-not-null
.
Đoạn mã Kotlin đã được viết lại như sau:
// Cách viết được khuyến khích
val order: Order? = getOrderById(orderId)
order?.let {
val valid = isCustomerValid(it.customer)
// ...
}
8. Hãy sử dụng lớp dữ liệu (data class)
Trong Kotlin, bạn có thể sử dụng lớp dữ liệu (data class
) để định nghĩa một đối tượng bất biến, rất phù hợp cho việc sử dụng đối tượng truyền giá trị (value object).
Ví dụ, đoạn mã Kotlin sau định nghĩa một lớp Email
dùng để gửi email:
// Cách viết được khuyến khích
data class Email(val to: String, val subject: String, val content: String)
interface EmailService {
fun send(email: Email)
}
Java phiên bản 14 cũng đã mượn ý tưởng từ Kotlin về data class
, giới thiệu từ khóa record
để định nghĩa lớp dữ liệu bất biến.
Đoạn mã tương ứng trong Java như sau:
public record Email(String to, String subject, String content) {}
public interface EmailService {
void send(Email email);
}
9. Hãy sử dụng hàm đơn biểu thức để ánh xạ trường
Xem đoạn mã Kotlin sau:
data class User(val name: String, val age: Int, val gender: String)
// Cách viết không được khuyến khích
fun parseMapToUser(userMap: Map<String, Any>): User {
return User(
name = userMap["name"] as String,
age = userMap["age"] as Int,
gender = userMap["gender"] as String
)
}
Đoạn mã trên trích xuất thông tin trường từ Map
để ghép thành đối tượng cụ thể. Nếu một hàm chỉ làm việc ánh xạ trường và chuyển đổi đối tượng như vậy, cách viết trên không được khuyến khích.
Sử dụng hàm đơn biểu thức để viết lại đoạn mã trên sẽ rõ ràng và dễ đọc hơn.
Mã như sau:
// Cách viết được khuyến khích
fun parseMapToUser(userMap: Map<String, Any>) = User(
name = userMap["name"] as String,
age = userMap["age"] as Int,
gender = userMap["gender"] as String
)
Ngoài ra, bạn cũng có thể sử dụng hàm mở rộng để thực hiện chức năng này.
// Cách viết được khuyến khích
fun Map<String, Any>.toUser() = User(
name = this["name"] as String,
age = this["age"] as Int,
gender = this["gender"] as String
)
10. Không nên khởi tạo thuộc tính trong khối init
Xem đoạn mã Java sau:
public class UserClient {
private String baseUrl;
private String usersUrl;
private CloseableHttpClient httpClient;
public UserClient(String baseUrl) {
this.baseUrl = baseUrl;
// Khởi tạo usersUrl
usersUrl = baseUrl + "/users";
// Khởi tạo httpClient
HttpClientBuilder builder = HttpClientBuilder.create();
builder.setUserAgent("UserClient");
builder.setConnectionManagerShared(true);
httpClient = builder.build();
}
public List<User> getUsers() {
// ...
}
}
Trong đoạn mã trên, một số thuộc tính là tham số của trình xây dựng, cần truyền vào khi gọi, trong khi các thuộc tính khác cần được ghép nối hoặc khởi tạo bên trong phương thức xây dựng. Chuyển đoạn mã này sang Kotlin, chúng ta sẽ có kết quả như sau:
// Cách viết không được khuyến khích
class UserClient(baseUrl: String) {
private val usersUrl = "$baseUrl/users"
private val httpClient: CloseableHttpClient
// Khởi tạo httpClient trong khối init
init {
val builder = HttpClientBuilder.create()
builder.setUserAgent("UserClient")
builder.setConnectionManagerShared(true)
httpClient = builder.build()
}
fun getUsers() {
// ...
}
}
Cách viết này không được khuyến khích. Trong Kotlin, bạn có thể trực tiếp sử dụng biểu thức đơn để khởi tạo thuộc tính ngay khi định nghĩa. Cách viết được khuyến khích như sau:
// Cách viết được khuyến khích
class UserClient(baseUrl: String) {
private val usersUrl = "$baseUrl/users"
// Khởi tạo httpClient ngay khi định nghĩa
private val httpClient = HttpClientBuilder.create().apply {
setUserAgent("UserClient")
setConnectionManagerShared(true)
}.build()
fun getUsers() {
// ...
}
}
11. Sử dụng object để khai báo triển khai giao diện không trạng thái
Nếu một lớp không có trạng thái, chỉ dùng để triển khai giao diện, thì rất phù hợp để khai báo nó là object
.
Ví dụ sử dụng object
như sau:
object DefaultListener : MouseAdapter() {
override fun mouseClicked(e: MouseEvent?) {
// ...
}
override fun mouseReleased(e: MouseEvent?) {
// ...
}
}
Trong đoạn mã trên, MouseAdapter
là một lớp trừu tượng trong Java AWT, và DefaultListener
triển khai hai phương thức của nó.
12. Sử dụng giải cấu trúc khi cần
Trong Java, không hỗ trợ trả về nhiều giá trị từ một phương thức, cũng không hỗ trợ mang nhiều giá trị trong các biến, điều này gây bất tiện trong thực tế.
Kotlin tuy không có chức năng trả về nhiều giá trị, nhưng hỗ trợ giải cấu trúc và các lớp dữ liệu tích hợp như Pair
và Triple
, có thể đạt được hiệu quả tương tự.
Xem đoạn mã Kotlin sau:
fun getStudents(): List<Pair<String, Int>> =
listOf(Pair("Larry", 28), Pair("Lucy", 26))
fun main() {
for ((name, age) in getStudents()) {
println("$name, $age")
}
}
Trong đoạn mã trên, sử dụng lớp tích hợp Pair
để hỗ trợ trả về hai giá trị, và lớp này cũng hỗ trợ giải cấu trúc, do đó có thể sử dụng cú pháp (name, age) = xxx
để lấy nhiều giá trị cùng lúc.
Đối với ba giá trị trở lên, có thể sử dụng lớp Triple
. Đối với nhiều giá trị hơn nữa, có thể định nghĩa lớp dữ liệu tùy chỉnh để đạt được mục đích tương tự, cũng hỗ trợ giải cấu trúc.
Ví dụ sử dụng giải cấu trúc với lớp dữ liệu tùy chỉnh như sau:
data class Student(val name: String, val age: Int, val gender: String, val grade: Int)
fun getStudents(): List<Student> =
listOf(
Student("Larry", 28, "Nam", 3),
Student("Lucy", 26, "Nữ", 2)
)
fun main() {
for ((name, age, gender, grade) in getStudents()) {
println("$name, $age, $gender, $grade")
}
}
13. Sử dụng lớp kín để thay thế việc sử dụng ngoại lệ
Xem đoạn mã Kotlin sau:
// Cách viết không được khuyến khích
data class User(
val id: Long,
val avatarUrl: String,
val name: String,
val email: String
)
@Throws(UserException::class)
fun requestUser(id: Long): User = try {
restTemplate.getForObject<User>("...")
} catch (ex: IOException) {
throw UserException(
message = "parse_failed",
cause = ex
)
} catch (ex: RestClientException) {
throw UserException(
message = "request_failed",
cause = ex
)
}
Trong đoạn mã trên, hàm requestUser
sử dụng restTemplate
để gọi API REST và lấy thông tin người dùng. Nếu xảy ra lỗi trong quá trình gọi, ngoại lệ sẽ được bọc lại thành UserException
và ném ra.
Đoạn mã này có thể được viết lại bằng cách sử dụng lớp kín (sealed class
) trong Kotlin.
Đoạn mã đã được viết lại như sau:
// Cách viết được khuyến khích
data class User(
val id: Long,
val avatarUrl: String,
val name: String,
val email: String
)
sealed class UserResponse {
data class Success(val user: User) : UserResponse()
data class Error(val code: String, val description: String) : UserResponse()
}
fun requestUser(id: Long): UserResponse = try {
val user = restTemplate.getForObject<User>("...")
UserResponse.Success(user = user)
} catch (ex: IOException) {
UserResponse.Error("parse_failed", "${ex.message}")
} catch (ex: RestClientException) {
UserResponse.Error("request_failed", "${ex.message}")
}
14. Đừng để mức lồng nhau của if-else vượt quá 3 tầng
Trong Java, yêu cầu mức lồng nhau của if-else
không vượt quá 3 tầng.
Ví dụ, quy tắc trong Alibaba Java Coding Guidelines khuyến nghị rằng nếu phải sử dụng if()...else if()...else...
để biểu diễn logic, tránh khó khăn trong việc bảo trì mã sau này, đừng vượt quá 3 tầng.
Xem đoạn mã Java sau:
// Cách viết không được khuyến khích
public Long getPriceTotalByOrderId(Long orderId) throws BusinessException {
long priceTotal = 0L;
// Truy vấn đơn hàng
Order order = orderService.getOrderById(orderId);
if (null != order) {
// Truy vấn sản phẩm trong đơn hàng
List<Product> products = order.getProducts();
if (!products.isEmpty()) {
// Tính tổng giá trị sản phẩm
for (Product product : products) {
if (!product.isGift()) {
priceTotal += product.getPrice();
}
}
} else {
throw new BusinessException("Đơn hàng không chứa sản phẩm nào");
}
} else {
throw new BusinessException("Không tìm thấy đơn hàng tương ứng");
}
return priceTotal;
}
Đoạn mã trên sử dụng if-else
lồng nhau ở mức 3 tầng, điều này rất phổ biến trong mã mà chúng ta thường gặp.
Nếu sử dụng câu lệnh bảo vệ (guard statement) để cải tiến, đoạn mã trên sẽ trở thành dạng sau:
// Cách viết được khuyến khích
public Long getPriceTotalByOrderId(Long orderId) throws BusinessException {
long priceTotal = 0L;
// Truy vấn đơn hàng
Order order = orderService.getOrderById(orderId);
if (null == order) {
throw new BusinessException("Không tìm thấy đơn hàng tương ứng");
}
// Truy vấn sản phẩm trong đơn hàng
List<Product> products = order.getProducts();
if (products.isEmpty()) {
throw new BusinessException("Đơn hàng không chứa sản phẩm nào");
}
// Tính tổng giá trị sản phẩm
for (Product product : products) {
if (!product.isGift()) {
priceTotal += product.getPrice();
}
}
return priceTotal;
}
Như vậy, đoạn mã đã được cải tiến, giảm mức lồng nhau của if-else
từ 3 tầng xuống còn một tầng duy nhất, làm cho logic rõ ràng hơn và giảm khả năng xuất hiện lỗi.
Trong Kotlin cũng vậy, nên tránh sử dụng if-else
lồng nhau ở mức 3 tầng hoặc nhiều hơn:
// Cách viết không được khuyến khích
@Throws(BusinessException::class)
fun getPriceTotalByOrderId(orderId: Long): Long {
var priceTotal = 0L
// Truy vấn đơn hàng
val order: Order? = DefaultOrderService().getOrderById(orderId)
return if (null != order) {
// Truy vấn sản phẩm trong đơn hàng
val products: List<Product> = order.products
if (products.isNotEmpty()) {
// Tính tổng giá trị sản phẩm
for (product in products) {
if (!product.isGift()) { // Chỉ tính sản phẩm không phải quà tặng
priceTotal += product.price
}
}
priceTotal
} else {
throw BusinessException("Đơn hàng không chứa sản phẩm nào")
}
} else {
throw BusinessException("Không tìm thấy đơn hàng tương ứng")
}
}
Thay vào đó, nên cố gắng làm phẳng cấu trúc if-else
:
// Cách viết được khuyến khích
@Throws(BusinessException::class)
fun getPriceTotalByOrderId(orderId: Long): Long {
// Truy vấn đơn hàng
val order: Order = DefaultOrderService().getOrderById(orderId) ?: throw BusinessException("Không tìm thấy đơn hàng tương ứng")
// Truy vấn sản phẩm trong đơn hàng
val products: List<Product> = order.products
if (products.isEmpty()) {
throw BusinessException("Đơn hàng không chứa sản phẩm nào")
}
// Tính tổng giá trị sản phẩm
return products.filterNot { it.isGift() }
.sumOf { it.price }
}
15. Không nên sử dụng System.out.println() để in nhật ký trong môi trường sản xuất
Không nên sử dụng System.out.println()
hoặc các lệnh đầu ra chuẩn khác để in nhật ký trong môi trường sản xuất, mà nên sử dụng gói nhật ký chuyên dụng.
Ví dụ, trong Alibaba Java Coding Guidelines, phần quy tắc nhật ký và ngoại lệ khuyến nghị rằng không nên sử dụng System.out
, System.err
hoặc e.printStackTrace()
để in nhật ký hoặc ngăn xếp ngoại lệ trong môi trường sản xuất.
Ví dụ sử dụng Slf4j để in nhật ký trong Java:
// Cách viết được khuyến khích
public class UserServiceImpl implements UserService {
private static final Logger logger = LoggerFactory.getLogger(UserServiceImpl.class);
@Override
public void save(User user) {
try {
// ...
} catch (Exception e) {
logger.error("Lưu người dùng thất bại", e);
}
}
}
Ví dụ sử dụng Slf4j để in nhật ký trong Kotlin:
// Cách viết được khuyến khích
class UserServiceImpl : UserService {
companion object {
private val logger = LoggerFactory.getLogger(UserServiceImpl::class.java)
}
override fun save(user: User) {
try {
// ...
} catch (e: Exception) {
logger.error("Lưu người dùng thất bại", e)
}
}
}
Tóm lại, bài viết này đã so sánh và tóm tắt một số phương pháp viết thông dụng và thực hành tốt nhất trong Kotlin bằng cách đối chiếu với Java, hy vọng sẽ giúp ích cho những người mới học Kotlin từ Java.
[1] Idioms | Kotlin Documentation - kotlinlang.org
[2] Idiomatic Kotlin Best Practices | Philipp Hauer’s Blog - phauer.com
[3] Alibaba Java Development Manual Huangshan Edition | Alibaba P3C - github.com