+8

Record trong Java 17 - giải pháp thay thế Lombok giúp mã sạch sẽ hơn?

Trong thế giới lập trình Java, việc tạo ra các lớp dữ liệu đơn giản thường gặp phải sự lặp lại và phiền toái. Lombok người bạn quen thuộc của chúng ta từ lâu đã giúp giảm bớt công việc này với các phương thức tự động hóa, nhưng sự ra mắt chính thức của Records trong Java 17 mở ra một lối đi mới, một cách tiếp cận tích hợp và hiệu quả hơn. Trong bài viết này, chúng ta sẽ khám phá sự xuất hiện của Record trong Java 17 và lý do tại sao nó có thể là lựa chọn thông minh để làm cho mã của bạn trở nên sạch sẽ và dễ đọc hơn.

1. Tổng quan

Bây giờ mình và các bạn cùng nhau tạo một đối tượng User để lưu trữ thông tin về người dùng và nó gồm các trường id, username, email

public class User {
    private int id;
    private String username;
    private String email;

    public User(int id, String username, String email) {
        this.id = id;
        this.username = username;
        this.email = email;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        User user = (User) o;

        if (id != user.id) return false;
        if (username != null ? !username.equals(user.username) : user.username != null) return false;
        return email != null ? email.equals(user.email) : user.email == null;
    }

    @Override
    public int hashCode() {
        int result = username != null ? username.hashCode() : 0;
        result = 99 * result + (email != null ? email.hashCode() : 0);
        result = 99 * result + id;
        return result;
    }

    @Override
    public String toString() {
        return "User{" +
                "username='" + username + '\'' +
                ", email='" + email + '\'' +
                ", id=" + id +
                '}';
    }
}

Nhìn khá khủng khiếp đúng không. Với 3 trường thuộc tính mà đã sinh tới 64 dòng code

Lúc bấy giờ, Lombok giáng thế:

  • Chắc hẳn ai cũng biết Lombok là một thư viện Java nhẹ được thiết kế để giảm lặp mã và tăng cường tính dễ bảo trì của mã nguồn. (Tham khảo: 16 Lombok Annotations trong 4 phút)

  • Thư viện này giúp giảm việc phải viết các phương thức standard như getters, setters, constructors, toString(), equals(), và hashCode(), ... bằng cách sử dụng các annotation.

Và áp dụng lên ví dụ ở trên, bạn hoàn toàn có thể giảm 64 dòng code xuống chỉ còn 5 dòng:

import lombok.Data;

@Data
public class User {
    private int id;
    private String username;
    private String email;
}

Việc sử dụng @Data tương đương với việc bạn đang sử dụng @Getters, @Setters, @ToString, @EqualsAndHashCode@RequiredArgsConstructor. Bạn cũng có thể sử dụng các chú thích này riêng lẻ nếu cần.

Các vấn đề với Lombok: Lombok là một thứ viên tuyệt vời, tuy nhiên nó cũng không phải hoàn hảo 100%, cũng có rất nhiều lý do để bạn cân nhắc có nên sử dụng Lombok trong dự án hay không. Nhưng theo mình thấy có hai lý do chính sau đây. Có thể nó không phải lý do quá lớn, nhưng nếu đặt nó cạnh Records thì đây lại có thể là lí do đầu tiên khiến bạn phải cân nhắc việc thay đổi nó

  • Sự phụ thuộc của bên thứ ba: Đầu tiên, rõ ràng Lombok là thư viện của bên thứ 3, không phải do trực tiếp Java phát triển, tức là nó không phải một phần của Java. Và chúng ta đang phải sử dụng thư viện của bên thứ 3 để làm một công việc hết sức đơn giản và lặp đi lặp lại trong mã nguồn của mình. Thứ 2, Lombok dựa vào sự hỗ trợ của cộng đồng để bảo trì thư viện. Nếu có vấn đề về khả năng tương thích với các phiên bản Java mới hơn hoặc nếu thư viện không được hỗ trợ thì điều này hoàn toàn có thể gây ra lỗi trong code base của bạn.

  • Khả năng tương thích IDE: Lombok dựa vào việc tạo code tại thời điểm biên dịch (compile-time), điều này có thể không phải lúc nào cũng hoạt động chính xác trơn tru với tất cả các Môi trường phát triển tích hợp (IDE). Một số IDE có thể không hỗ trợ đầy đủ các tính năng của Lombok, khiến việc điều hướng và hiểu code được tạo trở nên khó khăn.

    • Có thể ví dụ nó đơn giản như sau: Ví dụ, khi sử dụng Lombok để tạo ra các phương thức getter và setter cho một trường, thực tế các phương thức này không được viết trực tiếp trong mã nguồn. Thay vào đó, Lombok sử dụng các annotation như @Getter@Setter để tạo ra các phương thức này một cách tự động trong quá trình biên dịch. Khi bạn xem mã nguồn trong IDE, các phương thức này có thể không được hiển thị hoặc không được điều hướng một cách chính xác, làm giảm khả năng hiểu và tương tác với mã nguồn.

    • Ngoài ra, trong một số trường hợp, IDE có thể không hiển thị thông tin chi tiết về các trường hoặc phương thức được tạo ra bởi Lombok khi bạn di chuyển con trỏ chuột qua chúng, điều này làm giảm khả năng hiểu cũng như tương tác với mã nguồn.

Và giờ chúng ta sẽ cùng đến với giải pháp tốt hơn, và cũng là nhân vật chính của bài viết hôm nay.

2. Record - ngôi sao đang lên

Record là một tính năng mới được giới thiệu từ phiên bản Java 14 trở đi (dưới dạng tính năng xem trước) và ra mắt chính thức trong phiên bản Java 16. Record giúp đơn giản hóa việc tạo các lớp dữ liệu không thể thay đổi (immutable data classes). Chúng là một loại lớp tự động tạo ra các phương thức phổ biến như hàm tạo, equals(), hashCode()và toString()dựa trên các trường của lớp.

Nó giúp rút ngắn mã, tạo ra code rõ ràng hơn và giảm thiểu các bước cần thiết để tạo ra các class đơn giản chỉ chứa dữ liệu

Bạn có thấy sự giống nhau giữa Record và Lombok không? Cả hai đều giúp chúng ta đóng gói dữ liệu, và tự động tạo các phương thức phổ biến.

Ví dụ về cách sử dụng record:

public record UserRecord(int id, String username, String email) {}

Đúng như những gì kì vọng. Chỉ với một dòng duy nhất, chúng ta đã rút ngắn từ 64 dòng ban đầu với code truyền thống, 5 dòng với Lombok và hoàn toàn không phải phụ thuộc vào thư viện bên thứ 3.

Sau khi chúng ta tạo ra lớp trên, Java sẽ xác định 3 biến final là id, username, email và các phương thức getter của chúng, bên cạnh đó là các phương thức như toString, hashCode và equals.

Chi tiết về Record:

Khi bạn đã tạo lớp UserRecord, bạn có thể bắt đầu sử dụng ngay nó.

// Khởi tạo.
UserRecord userRecord = new UserRecord("999","bach", "ngocbachnguyen@gmail.com");
// Lấy các thuộc tính
System.out.println(userRecord.email());
System.out.println(userRecord.toString());

Các bạn cần chú ý rằng sẽ không có các phương thức getter. Chúng ta sẽ sử dụng trực tiếp tên biến làm tên phương thức.

Ví dụ: thay getEmail() với cách sử dụng truyền thống bằng việc chỉ sự dụng email() khi gọi các phương thức của Record.

Chúng ta không thể đặt lại giá trị cho thuộc tính của Record sau khi khởi tạo vì tất cả các biến là final. Điều này có nghĩ là Record là bất biến (immutable).

Ví dụ:

public record UserRecord(int id, String username, String email) {}

public class Main {
    public static void main(String[] args) {
        UserRecord user1 = new UserRecord(1, "john_doe", "john@example.com");
        UserRecord user2 = new UserRecord(2, "alice_smith", "alice@example.com");

        System.out.println("User 1: ID=" + user1.id() + ", Username: " + user1.username() + ", Email: " + user1.email());
        System.out.println("User 2: ID=" + user2.id() + ", Username: " + user2.username() + ", Email: " + user2.email());

        // Không thể thay đổi các thuộc tính của record vì chúng là immutable
        // user1.username = "new_username"; // Sẽ báo lỗi biên dịch

        // Có thể so sánh các record với nhau
        System.out.println("User 1 equals User 2? " + (user1.equals(user2)));
    }
}

Bạn cũng có thể khai báo static methodinstance method bên trong 1 Record hoặc khai báo biến static. Tuy nhiên bạn không thể khai báo instance variables.

Ví dụ:

// static variable
  public static final String invalidEmailMessage = "INVALID EMAIL";

  // instance variable - sẽ không khai báo được
  public String defaultEmail = "noname@gmail.com";

  // static method
  public static void sayMyName() {
    System.out.println("NgocBach");
  }

  // instance function
  public String emailDomain() {
    return this.email.split("@")[1];
  }
// gọi instance method sử dụng object
userRecord.emailDomain();
// gọi static method sử dụng Class.
UserRecord.sayMyName();

Tiếp nữa, class Record không thể được mở rộng (extend) hoặc mở rộng một lớp khác. Record được thiết kế để đơn giản hóa việc tạo ra các lớp dữ liệu không thể thay đổi (immutable data classes) và chúng không hỗ trợ tính năng kế thừa.

Record Constructor:

Khi bạn khai báo một record, nó tự động tạo một hàm tạo mặc định (default constructor) với tất cả các tham số của record. Loại hàm tạo này được gọi là "hàm tạo chuẩn" (canonical constructor). Hàm tạo chuẩn này khớp với các trường dữ liệu trong record theo thứ tự mà chúng được khai báo.

Ví dụ, nếu bạn có một record như sau:

public record UserRecord(int id, String username, String email) {}

Thì Java tự động tạo một hàm tạo chuẩn như sau:

public UserRecord(int id, String username, String email) {
    this.id = id;
    this.username = username;
    this.email = email;
  }

Lưu ý rằng bạn không cần phải tự định nghĩa hàm tạo này trong code của bạn. Java sẽ tự động tạo nó cho bạn. Tuy nhiên, nếu bạn muốn định nghĩa một hàm tạo tùy chỉnh, bạn có thể làm điều đó và Java sẽ không tạo hàm tạo chuẩn nếu một hàm tạo tùy chỉnh đã được định nghĩa.

public UserRecord(int id, String username, String email) {
    this.id = id;
    this.username = username;
    this.email = email;
    if (id < 1) {
      throw new IllegalArgumentException("Bạn không thể sử dụng id < 1");
    }
  }

Bạn cũng có thể hoàn toàn tạo một constructor nhỏ gọn hơn bằng cách loại bỏ các chi tiết không cần thiết. Ví dụ constructor ở trên với logic tùy chỉnh có thể được viết gọn gàng như sau:

public UserRecord {
    if (id < 1) {
       throw new IllegalArgumentException("Bạn không thể sử dụng id < 1");
    }
  }

Như vậy chúng ta đã đi qua những gì mà Record có thể làm cũng như cách để sử dụng nó. Không biết hiện giờ các bạn có đang tự đặt câu hỏi "Liệu Record có thể thay thế được Lombok không?". Với những gì chúng ta đã tìm hiểu, cũng có thể dễ dàng nhận ra được rằng ở thời điểm hiện tại Record khá là khó để có thể thay thế hoàn toàn được Lombok vì chúng không cung cấp tính năng đa dạng và phong phú như Lombok. Lombok cung cấp nhiều tính năng hơn, bao gồm khả năng tạo getter, setter, constructor tùy chỉnh, và nhiều tính năng khác mà record không có. Tuy nhiên, với các trường hợp sử dụng đơn giản và cơ bản, record có thể thay thế Lombok một cách hiệu quả. Và bạn cũng hoàn toàn có thể sử dụng cả hai trong dự án Java của mình.

3. Kết luận

Record và Lombok đều cung cấp cách tiếp cận để tạo ra các lớp dữ liệu trong Java, nhưng mỗi công nghệ có những điểm mạnh và điểm yếu riêng. Record là một phần của ngôn ngữ Java từ phiên bản 14 trở đi và cung cấp một cú pháp ngắn gọn và tiêu chuẩn cho việc tạo các lớp dữ liệu không thể thay đổi. Trong khi đó, Lombok cung cấp tính năng linh hoạt hơn và nhiều tính năng khác nhau như tự động sinh code, builder pattern, và annotation-based configuration.

Khi quyết định sử dụng Record hoặc Lombok, bạn cần xem xét các yếu tố như yêu cầu của dự án, phiên bản Java mà dự án sử dụng, tính linh hoạt cần thiết, và sở thích cá nhân. Trong một số trường hợp, việc sử dụng cả hai công nghệ cùng một lúc có thể là một lựa chọn tốt để tận dụng được lợi ích của cả hai. Điều quan trọng là đảm bảo rằng code của bạn dễ đọc, dễ bảo trì và tương thích với các công cụ và môi trường phát triển của bạn.


All Rights Reserved

Viblo
Let's register a Viblo Account to get more interesting posts.