Khởi tạo object trong Java – có thật sự dễ?

Giới thiệu Khởi tạo object trong Java, một vấn đề cơ bản nhưng có khá nhiều khía cạnh để phân tích. Hãy cùng nhau điểm qua một vài phương pháp: sử dụng constructor/static method, pattern builder, annotation builder. Và cuối cùng, bài viết sẽ giới thiệu một hướng tiếp cận mới bằng cách sử dụng

Giới thiệu

Khởi tạo object trong Java, một vấn đề cơ bản nhưng có khá nhiều khía cạnh để phân tích. Hãy cùng nhau điểm qua một vài phương pháp: sử dụng constructor/static method, pattern builder, annotation builder. Và cuối cùng, bài viết sẽ giới thiệu một hướng tiếp cận mới bằng cách sử dụng chaining method + lambda expression.

Sử dụng Constructor

Cách đơn giản nhất: tạo constructor có số lượng argument tương ứng. Khi muốn thay đổi argument, chúng ta có thể thay đổi constructor hiện tại (cách này không an toàn lắm), hoặc tạo thêm constructor mới. Khi đó class của chúng ta sẽ xuất hiện khá nhiều constructor mà không rõ ngữ cảnh sử dụng.

publicBook(String isbn,String title){this.isbn = isbn;this.title = title;}publicBook(String isbn,String title,String author){this.isbn = isbn;this.title = title;this.author = author;}publicBook(String isbn,String title,String author,String subCategory,Category category){this.isbn = isbn;this.title = title;this.author = author;this.subCategory = subCategory;this.category = category;}

Để giải quyết việc này, chúng ta có thể chuyển sang static method.

publicstaticBookquickInit(String isbn,String title){Book book =newBook();
    book.isbn = isbn;
    book.title = title;return book;}publicstaticBookfullInit(String isbn,String title,String author,String subCategory,Category category){Book book =newBook();
    book.isbn = isbn;
    book.title = title;
    book.author = author;
    book.subCategory = subCategory;
    book.category = category;return book;}

Sử dụng pattern/annotation Builder

Tuy nhiên, việc dùng constructor vẫn kém linh hoạt trong trường hợp chúng ta muốn tùy biến số lượng argument. Để giải quyết vấn đề đó, chúng ta có thể sử dụng pattern Builder (chỉ phù hợp khi business logic của bạn thật sự phức tạp), hoặc đơn giản hơn là dùng annotation @Builder của Lombok. Cách này cho phép ta hoàn toàn tùy biến số lượng argument muốn khởi tạo.

Book book =Book.builder().title("Sapiens: A Brief History of Humankind").author("Yuval Noah Harari").subCategory("History").build();

Bạn có thể thắc mắc, tại sao phải cầu kì như vậy, có thể dùng constructor rỗng và setter là xong mà? Hãy thử nghĩ đến trường hợp bạn chỉ muốn khai báo trong một dòng duy nhất như dưới đây:

List<Book> nonFictions =Arrays.asList(Book.builder().title("Sapiens: A Brief History of Humankind").author("Yuval Noah Harari").build(),Book.builder().title("The Defining Decade").author("Meg Jay").build(),Book.builder().title("The State of Affairs").author("Esther Perel").build());

Đặt vấn đề

Vẫn còn một vài bất tiện khi sử dụng annotation Builder. Đó là khi class của chúng ta có sử dụng generic type parameter.

Xem xét ví dụ sau, chúng ta muốn xuất dữ liệu của một danh sách các object bất kì dưới dạng bảng.

Ta sẽ định nghĩa các Column của bảng tương ứng với mỗi field bên trong object.

publicclassColumn<T>{String title;String fieldName;Function<T,?> customExtractor;}

Khi đó một Table sẽ là list các Column của đối tượng mà ta muốn xuất dữ liệu.

Table<Book>=[Column("Book ID","isbn"),Column("Name","title"),Column("Category", book -> book.category.name)]

Đoạn mã giả này mô tả một table của class Book, với cột đầu là ID của cuốn sách lấy từ field isbn, cột hai là tên cuốn sách lấy từ field title, và cột cuối là thể loại sách lấy từ name của category của cuốn sách, chúng ta sử dụng một function để chỉ định cách lấy giá trị cột này.

Dưới đây là code tương ứng trong Java nếu áp dụng annotation @Builder.

List<Column<Book>> bookTable =Arrays.asList(Column.<Book>builder().title("Book ID").fieldName("isbn").build(),Column.<Book>builder().title("Name").fieldName("title").build(),Column.<Book>builder().title("Category").customExtractor(book -> book.getCategory().getName()).build());

Để ý thấy, chúng ta luôn phải thêm type <Book> mỗi lần gọi .builder(), nếu không compiler sẽ không nhận diện được T trong Function<T, ?> customExtractor chính xác là class nào.

Giải pháp: Function as a builder

Chúng ta sẽ cần sửa lại class Column một chút để bắt chước tính năng của annotation @Builder, bằng cách khai báo chaining method ngay trong class gốc.

(Chaining method: một hàm trả về object đã gọi nó – return this)

publicclassColumn<T>{String title;String fieldName;Function<T,?> customExtractor;publicColumn<T>title(String title){this.title = title;returnthis;}publicColumn<T>fieldName(String fieldName){this.fieldName = fieldName;returnthis;}publicColumn<T>customValue(Function<T,?> customExtractor){this.customExtractor = customExtractor;returnthis;}}

Để compiler có khả năng resolve class T tại thời điểm runtime, chúng ta sẽ tạo 1 static method nhận vào một Function<Column<T>, Column<T>> – đây chính là yếu tố mấu chốt của bài toán khởi tạo object.

publicclassColumn<T>{...publicstatic<T>Column<T>add(Function<Column<T>,Column<T>> builder){return builder.apply(newColumn<>());}}

Function builder sẽ đảm nhiệm việc chuyển đổi object rỗng new Column<>() thành object có đầy đủ các tham số khởi tạo. Code của chúng ta sẽ có kết quả như sau:

List<Column<Book>> bookTables =Arrays.asList(add(c -> c.title("Book ID").fieldName("isbn")),add(c -> c.title("Name").fieldName("title")),add(c -> c.title("Category").customValue(book -> book.getCategory().getName())));

Code đã sạch đẹp, dễ đọc, gần như không có yếu tố dư thừa trùng lặp, gần giống với đoạn mã giả ban đầu. Thật thú vị, đúng không?

Đánh giá

1. Việc áp dụng kĩ thuật chaining method (bắt chước từ annotation @Builder) cho phép chúng ta khởi tạo object với số lượng argument tùy ý mà không cần khai báo nhiều constructor khác nhau.

2. Sử dụng Function<Entity, Entity> builder giúp chúng ta loại bỏ từ khóa new

  • Không còn phải lặp lại những dòng new khó chịu như new Class(), new Class<Type>(), new Class<>() hay Class.<Type>init(). Bản thân function đã tự động resolve generic type tại thời điểm runtime.
  • Khi dùng static method để khởi tạo object, chúng ta luôn phải tốn một khoản không gian kí tự cho tên class và tên static method khởi tạo: LongClassName.init()...
  • Trong khi đó, nhờ vào lambda expression, chúng ta chỉ phải tốn nhiều nhất 6 kí tự cho việc gọi hàm khởi tạo: o -> o... Bạn hoàn toàn có thể tự thay đổi tên param input của function để nó gợi nhớ hơn, hoặc đơn giản, cứ để ngắn gọn như vậy, bởi tên của method invoke đã quyết định ngữ cảnh rồi (như ví dụ Column.add() ở trên)

Kết

Sau khi tham khảo tư liệu để hỗ trợ cho bài viết này, mình thấy 1 trang có đề cập các design pattern hiện đại dành cho Java 8 bằng cách áp dụng lambda expression, tuy nhiên những kĩ thuật ở trang này cũng không thật sự giống với cách mình đang sử dụng.

Mình tình cờ tìm ra pattern này trong lúc code library hỗ trợ đọc/ghi Excel theo phong cách khai báo – mình là một fan cuồng declarative programming. Nếu bạn có hứng thú, ghé qua Github của mình cũng như dự án Excel mà mình đang làm nhé.

Tất nhiên, đây không phải là một phát hiện mới mẻ gì – bạn có thể tham khảo dự án SpringFu, JaFu của đội ngũ Spring, để xem cách họ apply pattern này. Mình nghĩ đây là một pattern khá tiện lợi mà bạn có thể áp dụng cho những dự án trong tương lai.

Liên kết ngoài

  1. Pattern Builder: https://refactoring.guru/design-patterns/builder
  2. Project Lombok – annotation Builder: https://projectlombok.org/features/Builder
  3. Creational patterns with Java 8: https://jaxenter.com/patterns-java-8-lambdas-127635.html
  4. Project SpringFu: https://github.com/spring-projects-experimental/spring-fu/tree/main/jafu
  5. Dự án của mình: https://github.com/nambach/ExcelUtil

Nguồn: viblo.asia

Bài viết liên quan

WebP là gì? Hướng dẫn cách để chuyển hình ảnh jpg, png qua webp

WebP là gì? WebP là một định dạng ảnh hiện đại, được phát triển bởi Google

Điểm khác biệt giữa IPv4 và IPv6 là gì?

IPv4 và IPv6 là hai phiên bản của hệ thống địa chỉ Giao thức Internet (IP). IP l

Check nameservers của tên miền xem website trỏ đúng chưa

Tìm hiểu cách check nameservers của tên miền để xác định tên miền đó đang dùn

Mình đang dùng Google Domains để check tên miền hàng ngày

Từ khi thông báo dịch vụ Google Domains bỏ mác Beta, mình mới để ý và bắt đầ