Né tránh lỗi tỉ đô

Null là một giá trị đặc biệt trong lập trình, và mỗi ngôn ngữ lập trình lại có những cách đối xử khác nhau, với C/C++ có thể là crash luôn chương trình, với Java thì có thể ném ra NullPointerException với javascript thì có thể là undefined. Tuy là một giá trị đơn giản nhưng lại gây ra rất nhiều lỗi trên các sản phẩm của chúng ta mà có khi phải rất lâu mới bắt gặp và khi bắt gặp thì toàn gây thiệt hại rất lớn, nhẹ thì 1 chức năng không sử dụng được, nặng thì có thể làm tê liệt cả 1 dịch vụ. Vậy cần phải có 1 cách nào đó để chúng ta né tránh hoặc giảm thiểu được rủi ro đối với giá trị Null này, đó là chính là lúc Null Object Design Pattern ra đời.

Bài toán thực tế

Có lẽ bài toán phổ biến nhất mà chúng ta vẫn thường làm đó là truy xuất data từ cơ sở dữ liệu kiểu thế này:

public User getUserById(long userId) {}

Vậy đối với trrường hợp chúng ta không tìm thấy dữ liệu thì phải làm thế nào? Trả về null và viết docs cho hàm đó, hay chúng ta sẽ làm gì để cho những người sử dụng hàm này không được phép quên xử lý trường hợp không tìm thấy dữ liệu?

Mục tiêu ra đời

Để trả lời những câu hỏi đó thì Null Object Design Pattern đã ra đời với mục tiêu:

  • Đóng gói lại giá trị null, từ đó cố gắng gây sự chú ý cho các lập trình viên để xử lý các trường hợp không tìm thấy dữ liệu.
  • Tạo ra các xử lý mặc định trong trường hợp dữ liệu bị null, như trả về giá trị mặc định hay nén ra exception.

Mô hình tiêu chuẩn

Là design pattern đơn giản nhất trong số các pattern nên mô hình tiêu chuẩn của null object cũng chẳng có gì đặc biệt.

  1. CompositeOperation: chứa các opration để xử lý dữ liệu trong trường hợp null và không null
  2. RealOperation: xử lý dữ liệu trong trường hợp không null
  3. NullOperation: xử lý dữ liệu trong trường hợp null

Ví dụ

Để giải quyết bài toán đã nói ở trên, chúng ta sẽ sử dụng đối tượng Optional có sẵn kể từ java 8. Tuy nhiên hãy thiết kế sơ đồ lớp một chút cho dễ nhìn nhé.

Sơ đò lớp

Trong sơ đồ này thì:

  1. Optional sẽ đóng vai trò là 1 lớp wrapper, bao bọc lại kết quả, xử lý if else và gọi đến các opration tương ứng
  2. ToJsonOperation: biến đối tượng thành json string
  3. ToEmptyStringOperation: trong trường hợp dữ liệu bị null, sẽ trả về một chuỗi rỗng.
  4. UserFetcher: đóng vai trò là lớp sử dụng null object design pattern, nó sẽ lấy dữ liệu từ cơ sở dữ liệu và chuyển thành json và trả về cho người dùng.

Cài đặt

Trước tiên chúng ta sẽ cài đặt 2 lớp xử lý:

public class ToJsonOperation implements Function<User, String> {
    @Override
    public String apply(User t) {
        return t.toJson();
    }
}

public class ToEmptyStringOperation implements Supplier<String> {
    @Override
    public String get() {
        return "";
    }
}

Tiếp theo, việc sử dụng sẽ chỉ đơn giản thế này thôi:

String user1stJson = userFetcher
        .getUserById(1L)
        .map(new ToJsonOperation())
        .orElseGet(new ToEmptyStringOperation());
System.out.println(user1stJson);

String user3rdJson = userFetcher
        .getUserById(3L)
        .map(new ToJsonOperation())
        .get();
System.out.println(user3rdJson);

Source code đầy đủ, bạn có thể tham khảo tại Github của mình nhé.

Tổng kết

Về cơ bản thì null object design pattern là một cách thay thế cho việc if (null) then else truyền thống. Tuy nhiên lợi ích to lớn của nó là khiến cho lập trình viên chú ý hơn đến việc xử lý trong trường hợp dữ liệu bị null này, hoặc nếu họ quên không xử lý thì sẽ có hành động mặc định được đưa ra để dễ dàng debug hơn trong quá trình chạy thực tế ví dụ như ném ra 1 exception với 1 message dễ hiểu chẳng hạn.