Thứ Năm, 31 tháng 3, 2016

Java 8 Optional - tiếng súng từ bên kia chiến tuyến

Java 8 đã chính thức ra mắt từ 2 năm trước, có rất nhiều bài viết đề cập về những cải tiến cũng như vận dụng thực tế liên quan đã được viết trong suốt thời gian qua. Thật ngạc nhiên, trong vô số đặc điểm mới, một chủ đề gây tranh cãi nhiều nhất chính là lớp Optional.

Tác giả: Daniel Olszewski  
Dịch giả: Trương Thanh Tùng
Hiệu chỉnh: Lộc Hồ
Xin không copy dưới mọi hình thức

4 nhận xét:

  1. Bản chất của nó là một container, nó có thể rỗng hoặc chứa giá trị null. Cấu trúc này nhắc nhở người dùng Optional object có thể không có gì bên trong nó để xử lý một cách thích hợp. Mặc dù định nghĩa trên Javadoc đã trình bày khá đầy đủ, việc áp dụng đúng đắn vào những trường hợp cụ thể còn gặp nhiều vấn đề cần được quan tâm.

    Method Result

    Trường hợp đầu tiên được sử dụng chắc ai cũng đoán được. Brian Goetz người phát triển ngôn ngữ Java tại Oracle cho biết mục đích này trong câu trả lời của mình trên Stack Overflow là để thúc đẩy việc thêm kiểu dữ liệu “không kết quả” như đã nói ở trên vào trong thư viện chuẩn của Java.

    “Chủ ý của chúng tôi là cung cấp một cơ chế hạn chế với dữ liệu trả về của phương thức trong thư viện, nơi cần thiết phải thể hiện một cách rõ ràng “no result” (không kết quả), và kiểu dữ liệu null hoàn toàn có khả năng tạo lỗi rất cao.”

    Trước java 8, nhận giá trị null từ method là không rõ ràng. Nó có thể có nghĩa là giá trị được trả về không tồn tại hoặc xảy ra một lỗi trong quá trình thực thi phương thức. Bên cạnh đó, developer có xu hướng quên kiểm tra nếu kết quả là null, điều này dẫn đến phiền phức từ lỗi NullPointerException khi chạy. Tùy chọn giải quyết cả 2 vấn đề này bằng cách buộc người dùng phải kiểm tra kết quả đầu ra khá rõ ràng của method.

    Collection Wrapper – Một Ý tưởng tồi

    Trong khi đặt giá trị trả về có-thể-null (nullable) vào bên trong Optional là đáng được khuyến khích, qui định này lại không nên áp dụng khi đầu ra là một tập hợp hay một mảng. Trong trường hợp này, khi không có giá trị trả về, việc sử dụng thực thể rỗng (mảng rỗng hoặc tập rỗng) tốt hơn việc dùng Optional rỗng hoặc null bởi bản thân nó đã truyền tải hết những thông tin cần thiết. Optional không cung cấp hơn được điều gì mà lại chỉ gây bối rối cho mã triệu gọi (client code), do đó nên tránh trường hợp này. Code dưới đây là một minh hoạ cho sự bối rối đó:

     Optional> itemsOptional = getItems();
     if (itemsOptional.isPresent()) {
        // do we really need this?
        itemsOptional.get().forEach(item -> {
          // process item
        });
     } else {
        // the result is empty
     }

    Hàm getItems() trả về một tập dữ liệu. Hàm isPresent() của đối tượng thuộc lớp Optional kiểm tra kết quả có hay không? Nếu có, sẽ thực hiện một vòng lặp để làm tiếp logic code. Lệnh else sẽ được xử lý nếu không có kết quả trả về. Thực tế chúng ta không cần phải phức tạp như vậy, hàm trả về một tập thì có thể gọi method isEmpty() để kiểm tra tập rỗng hay không, hoặc với mảng chúng ta sử dụng length. Optional là không cần thiết ở đây.

    Trả lờiXóa
  2. Constructor và Method Parameters

    Bạn có thể sử dụng Optional làm đối số đầu vào cho hàm hoặc constructor nhưng nếu so sánh với các giải pháp khác, nó chưa chắc đã hay hơn. Để minh hoạ cho điều này, xem xét một khai báo constructor dưới đây:

     public SystemMessage(String title, String content,
          Optional attachment) {
         // assigning field values
     }

    Thoạt nhìn, nó có vẻ như một thiết kế tốt với tham số cuối cùng là tùy chọn. Tuy nhiên, mọi thứ trở nên khó nhìn hơn khi ta khởi tạo đối tượng cho SystemMessage:

     SystemMessage withoutAttachment = new SystemMessage("title", "content", Optional.empty());
     Attachment attachment = new Attachment();
     SystemMessage withAttachment = new SystemMessage("title", "content", Optional.ofNullable(attachment));

    Thay vì cung cấp đầu vào một cách rõ ràng, the factory methods của Optional class chỉ thêm bối rối cho người đọc mã. Lưu ý, đây mới chỉ có 1 tham số optional, nhưng hãy tưởng tượng 2 hoặc 3. Bác Bob chắc chắn sẽ chẳng tự hào với mã lệnh như vậy.

    Khi một method có thể chấp nhận Optional parameters, nó sẽ thích hợp hơn trong việc sử dụng nạp chồng (overloading) ở đây. Với ví dụ trên, việc khai báo 2 constructor khác nhau về đối số đầu vào sẽ là lựa chọn hợp lý:

     public SystemMessage(String title, String content) {
        this(title, content, null);
     }

     public SystemMessage(String title, String content, Attachment attachment) {
        // assigning field values
     }


    Code khởi tạo đối tượng sẽ rõ ràng hơn như dưới đây:

     SystemMessage withoutAttachment = new SystemMessage("title", "content");

     Attachment attachment = new Attachment();
     SystemMessage withAttachment = new SystemMessage("title", "content", attachment);

    Trả lờiXóa
  3. POJO - Trường hợp gây tranh cãi

    Getter và setter trong POJO (Plain Old Java Object) có lẽ gây ra nhiều tranh cãi nhất khi sử dụng Optional, đánh giá từ các bài blog, bài viết, thảo luận,… chúng ta chỉ đồng ý một điều: một cuộc thánh chiến khác đã được bắt đầu. Ở trường hợp khác, trong bài post trên StackOverflow nói trên, Brian Goetz đã không có chút hoài nghi về tính hữu dụng của nó (Optional) cho accessors.

    “Tôi nghĩ thường xuyên sử dụng nó cho kiểu dữ liệu trả về của getter chắc chắn sẽ rất hợp lý”

    Điểm trừ đáng chú ý nữa là Optional vô tình không được cài đặt Serializable Interface, điều này khiến nó có thể bị loại bỏ khi lựa chọn làm kiểu dữ liệu cho các property (thuộc tính) của model. Vậy có đáng để sử dụng Optional cho các POJO field?

    Nhìn ở khía cạnh khác, một số người bắt đầu đặt câu hỏi với những lập luận trên. Một sự thật cần được nhìn nhận, lý do sử dụng nullable model field không phải là hiếm gặp và Optional hoàn toàn hợp lý.  Stephen Colebourne, người có công đóng góp chính trong việc vạch ra những yêu cầu đặt tả của thư viện Joda-Time và the JSR-310 đưa ra một cách dùng với Optional:

    Đặt những trường (fields) có thể có giá trị null (nullable) bên trong một lớp (class) song đóng gói chúng vào Optional khi được truy xuất thông qua public getters. Với mục đích sử dụng tương tự như kết quả (result) trả về của method, chúng tôi muốn lập trình viên chú ý đến khả năng giá trị (value) có thể không tồn tại.

    Bên cạnh lãng phí bộ nhớ, trở ngại chính trong việc sử dụng Optional như một trường của POJO là sự hỗ trợ của thư viện và các framework. Phản ứng thường thấy là đọc và thao tác với objects và Optional cần một sự xử lý đặc biệt. Ví dụ, đội ngũ xây dựng Jackson (thư viện mapping giữa object và định dạng Json) đã cung cấp phần mở rộng để xử lý Optional fields khi chuyển một POJO sang định dạng JSON. Hibernate Validator cũng đã làm việc với Optional entity fields nhưng rất nhiều trường hợp, bạn sẽ không tìm thấy sự hỗ trợ hoặc buộc phải thêm vào vài thành phần bổ sung là điều không tránh khỏi.

    Nếu bạn đi ngược những lời khuyên của Brian Goetz, bạn phải chắc chắn rằng tất cả các thư viện và framework cần thiết mà bạn dùng hoàn toàn xử lý được với Optional class. Không quan trọng đội của bạn quyết định thế nào, việc khởi động dự án mới cần đảm bảo tính xuyên suốt và thống nhất trong toàn bộ project.

    Trả lờiXóa
  4. Sự phụ thuộc của lớp tùy chọn

    Vài tính năng hoặc phần nào đó trong nghiệp vụ (business logic) có thể bật/tắt dựa vào cấu hình ứng dụng. Những mã lệnh xử lý đó có thể được viết trong các class riêng biệt và nó trở thành một tuỳ chọn khi vận hành ứng dụng. Stateless không cài đặt Serializable interface (bạn nhớ khái niệm về Stateless và Stateful mà ông Rời nói chứ?), những trường hợp như vậy có thể xem xét sử dụng Optional cho class field như ví dụ dưới đây:

     public class OrderProcessor {
        private Optional smsNotifier = Optional.empty();
        public void process(final Order order) {
            // some processing logic here
            smsNotifier.ifPresent(n -> n.sendConfirmation(order));
        }
        public void setSmsNotifier(SmsNotifier smsNotifier) {
            this.smsNotifier = Optional.ofNullable(smsNotifier);
        }
     }

    Nếu nullable field được sử dụng, chúng ta có rủi ro khi ai đó kế thừa class mà không xác nhận được sự hiện diện của nó khi sử dụng trường. Trong thế giới ràng buộc lẫn lộn bởi Dependency Injection frameworks, vô số nhà phát triển sẽ quan niệm rằng một trường nào đó trong business logic class sẽ được đặt (set) bởi container. Với trường hợp này, kiểu Optional thể hiện được thế mạnh và ngăn chặn các hành vi vô thức xảy ra.

    Thường trong lập trình, sẽ không có cách giải quyết tốt nhất cho vấn đề. Ở một vị thế khác, Optional Dependencies đóng vai trò của Null Object pattern, nó ưa chuộng hơn với một vài hoàn cảnh.

    Optional không phải “viên đạn bạc”.

    Mặc dù trong nhiều tình huống, việc chọn Optional chất chứa sự cám dỗ mạnh mẽ, và ý tưởng ban đầu cho kiểu dữ liệu này không hẳn được tạo ra để thay thế cho mọi mục đích của nullable value. Trước khi áp dụng nó, xem xét mục đích thay thế như là lạm dụng Optional dẫn tới sự nồng nặc của mùi mã lệnh - code smells (ám chỉ mã xấu, rối, viết khó hiểu, ngu ngốc). Nếu bạn có kinh nghiệm làm việc với Optional, đặc biệt với các trường của POJO hay các getter outputs, tôi rất hân hạnh được đọc về các bài học mà bạn đã gặp. Tôi đánh giá cao mọi bình luận, đừng do dự chia sẻ những khen chê của bạn.

    Tác giả Daniel Olszewski
    Dịch giả: Trương Thanh Tùng
    Hiệu chỉnh: Lộc Hồ
    Nguồn bài viết DZONE

    Trả lờiXóa

nhudinhthuan@gmail.com