- vừa được xem lúc

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

0 0 54

Người đăng: Nam Bach

Theo Viblo Asia

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.

public Book(String isbn, String title) { this.isbn = isbn; this.title = title;
} public Book(String isbn, String title, String author) { this.isbn = isbn; this.title = title; this.author = author;
} public Book(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.

public static Book quickInit(String isbn, String title) { Book book = new Book(); book.isbn = isbn; book.title = title; return book;
} public static Book fullInit(String isbn, String title, String author, String subCategory, Category category) { Book book = new Book(); 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.

Xuất data 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.

public class Column<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)

public class Column<T> { String title; String fieldName; Function<T, ?> customExtractor; public Column<T> title(String title) { this.title = title; return this; } public Column<T> fieldName(String fieldName) { this.fieldName = fieldName; return this; } public Column<T> customValue(Function<T, ?> customExtractor) { this.customExtractor = customExtractor; return this; }
}

Để 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.

public class Column<T> { ... public static <T> Column<T> add(Function<Column<T>, Column<T>> builder) { return builder.apply(new Column<>()); }
}

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

Bình luận

Bài viết tương tự

- vừa được xem lúc

Tổng hợp các bài hướng dẫn về Design Pattern - 23 mẫu cơ bản của GoF

Link bài viết gốc: https://gpcoder.com/4164-gioi-thieu-design-patterns/. Design Patterns là gì. Design Patterns không phải là ngôn ngữ cụ thể nào cả.

0 0 302

- vừa được xem lúc

Học Spring Boot bắt đầu từ đâu?

1. Giới thiệu Spring Boot. 1.1.

0 0 278

- vừa được xem lúc

Cần chuẩn bị gì để bắt đầu học Java

Cần chuẩn bị những gì để bắt đầu lập trình Java. 1.1. Cài JDK hay JRE.

0 0 51

- vừa được xem lúc

Sử dụng ModelMapper trong Spring Boot

Bài hôm nay sẽ là cách sử dụng thư viện ModelMapper để mapping qua lại giữa các object trong Spring nhé. Trang chủ của ModelMapper đây http://modelmapper.org/, đọc rất dễ hiểu dành cho các bạn muốn tìm hiểu sâu hơn. 1.

0 0 194

- vừa được xem lúc

[Java] 1 vài tip nhỏ khi sử dụng String hoặc Collection part 1

. Hello các bạn, hôm nay mình sẽ chia sẻ về mẹo check String null hay full space một cách tiện lợi. Mình sẽ sử dụng thư viện Lớp StringUtils download file jar để import vào thư viện tại (link).

0 0 71

- vừa được xem lúc

Deep Learning với Java - Tại sao không?

Muốn tìm hiểu về Machine Learning / Deep Learning nhưng với background là Java thì sẽ như thế nào và bắt đầu từ đâu? Để tìm được câu trả lời, hãy đọc bài viết này - có thể kỹ năng Java vốn có sẽ giúp bạn có những chuyến phiêu lưu thú vị. DJL là tên viết tắt của Deep Java Library - một thư viện mã ng

0 0 139