Hướng dẫn Java Design Pattern – Null Object

Trong lập trình hướng đối tượng, chúng ta thường xuyên kiểm tra một đối tượng xem có bằng null hay không trước khi thực hiện các phương thức của đối tượng để tránh lỗi Null Pointer Exception (NPE). Null không phải là một đối tượng, nó là một giá trị, các thao tác trên so sánh một đối tượng với một giá trị, nó mất đi tính đối tượng trong lập trình hướng đối tượng. Các đối tượng null này cần được kiểm tra để đảm bảo rằng chúng không rỗng trong khi truy cập bất kỳ thành viên nào hoặc gọi bất kỳ phương thức nào. Điều này là do các thành viên hoặc phương thức không thể được gọi trên các đối tượng null. Để tránh vấn đề này, chúng ta có thể sử dụng Null Object Pattern. Thay vì sử dụng giá trị null, chúng ta trả về một Null Object thể hiện hành vi mặc định của đối tượng.

1. Null Object Pattern là gì?

The intent of a Null Object is to encapsulate the absence of an object by providing a substitutable alternative that offers suitable default do nothing behavior. In short, a design where nothing will come of nothing.

Null Object Pattern là một trong những Pattern thuộc nhóm hành vi (Behavior Pattern). Null Object pattern không phải là một Gang of Four Design Pattern.

Tư tưởng của Null Object là sử dụng một đối tượng Null đặc biệt để gói gọn sự vắng mặt của một thể hiện bằng cách cung cấp một sự thay thế hành xử theo cách thụ động phù hợp. Về cơ bản, thay vì sử dụng giá trị null, hãy tạo một đối tượng không có tác động đến bất cứ thứ gì. Từ đó, người sử dụng không cần quan tâm đến việc reference đó có null hay không? Vì code implement bên trong đã xử lý mặc định rồi.

Nó là một đối tượng có hành vi trung lập hoặc không rõ ràng. Nó được gọi là Null Object bởi vì theo mặc định, nó không làm gì cả và nó giúp tránh các trường hợp Null Pointer Exception.

2. Cài đặt Null Object Pattern như thế nào?

Các thành phần tham gia Null Object Pattern:

  • AbstractObject : định nghĩa các hành vi mà một đối tượng có thể có.
  • RealObject : một triển khai thực sự của AbstractObject thực hiện một số hành động thực tế.
  • NullObject : một triển khai không làm gì hoặc trả về giá trị mặc định của AbstractObject, để cung cấp một đối tượng không null cho Client.
  • Client : nhận được một triển khai của AbstractObject và sử dụng nó. Nó không thực sự quan tâm đó là một NullObject hoặc RealObject vì cả hai đều được sử dụng theo cùng một cách.

2.1. Ví dụ Null Object Pattern với ứng dụng tính thuế (Tax)

Giả sử chúng ta cần tính giá tiền của sản phẩm sau khi cộng thêm thuế theo từng quốc gia. Chúng ta có một danh sách các quốc gia có áp dụng thuế, các quốc gia không nằm trong danh sách thì mặc định thuế sẽ là 0.

Chương trình của chúng ta như sau:

Tax.java

package com.maixuanviet.patterns.behavioral.nullobject.tax;
 
public interface Tax {
 
    String getCountry();
 
    double apply(double price);
}

RealTax.java

package com.maixuanviet.patterns.behavioral.nullobject.tax;
 
public class RealTax implements Tax {
    private String country;
    private double vat;
 
    public RealTax(String country, double vat) {
        this.country = country;
        this.vat = vat;
    }
 
    @Override
    public String getCountry() {
        return country;
    }
 
    @Override
    public double apply(double price) {
        return price * vat;
    }
}

NullTax.java

package com.maixuanviet.patterns.behavioral.nullobject.tax;
 
public class NullTax implements Tax {
 
    private String country = "UNKNOWN_COUNTRY";
 
    public NullTax(String country) {
        if (country != null) {
            this.country = country;
        }
    }
 
    @Override
    public String getCountry() {
        return country;
    }
 
    @Override
    public double apply(double price) {
        return price * 1;
    }
}

TaxFactory.java

package com.maixuanviet.patterns.behavioral.nullobject.tax;
 
import java.util.HashMap;
import java.util.Map;
 
public class TaxFactory {
 
    private static final Map<String, Double> VATS = new HashMap<>();
 
    static {
        VATS.put("Switzerland", 1.3);
        VATS.put("Germany", 1.45);
        VATS.put("Vietnam", 1.1);
    }
 
    public static Tax getTaxByCountry(String country) {
        Double vat = VATS.get(country);
        if (vat != null) {
            return new RealTax(country, vat);
        }
        return new NullTax(country);
    }
}

NullObjectPatternExample.java

package com.maixuanviet.patterns.behavioral.nullobject.tax;
 
public class NullObjectPatternExample {
 
    public static void main(String[] args) {
 
        final double price = 1000;
        applyCountryTaxToPrice(price, "Switzerland");
        applyCountryTaxToPrice(price, "Germany");
        applyCountryTaxToPrice(price, "Vietnam");
        applyCountryTaxToPrice(price, "Thailand");
        applyCountryTaxToPrice(price, null);
    }
 
    public static void applyCountryTaxToPrice(double price, String country) {
        Tax tax = TaxFactory.getTaxByCountry(country);
        System.out.println(tax.getCountry() + ": " + tax.apply(price));
    }
}

Output của chương trình:

Switzerland: 1300.0
Germany: 1450.0
Vietnam: 1100.0
Thailand: 1000.0
UNKNOWN_COUNTRY: 1000.0

Như bạn thấy chương trình của chúng ta rất đơn giản, phía Client không cần quan tâm quốc gia đó có áp dụng Tax hay không (không kiểm tra null), có thể thực hiện tính toán ngay.

2.2. Ví dụ Null Object Pattern với ứng dụng Persistence

NullObjectPatternExample.java

package com.maixuanviet.patterns.behavioral.nullobject.persistence.checknull;

import com.maixuanviet.patterns.behavioral.nullobject.persistence.Persistence;

public class NullObjectPatternExample {

	private Persistence persistence;

	public void persistence(Persistence persistence) {
		this.persistence = persistence;
	}

	public String get() {
		if (persistence != null) {
			return persistence.get();
		}
		return null;
	}

	public void insert(String value) {
		if (persistence != null) {
			persistence.insert(value);
		}
	}

	public void update(String value) {
		if (persistence != null) {
			persistence.update(value);
		}
	}

	public void delete() {
		if (persistence != null) {
			persistence.delete();
		}
	}
}

EmptyPersistence.java

package com.maixuanviet.patterns.behavioral.nullobject.persistence.nullobject;

import com.maixuanviet.patterns.behavioral.nullobject.persistence.Persistence;

public class EmptyPersistence implements Persistence {

	private static Persistence persistence;

	public static Persistence getInstance() {
		if (persistence == null) {
			persistence = new EmptyPersistence();
		}
		return persistence;
	}

	private EmptyPersistence() {
		// Do nothing
	}

	@Override
	public void delete() {
		// Do nothing
	}

	@Override
	public void insert(String value) {
		// Do nothing
	}

	@Override
	public void update(String value) {
		// Do nothing
	}

	@Override
	public String get() {
		// Do nothing
		return null;
	}

}

NullObjectPatternExample.java

package com.maixuanviet.patterns.behavioral.nullobject.persistence.nullobject;

import com.maixuanviet.patterns.behavioral.nullobject.persistence.Persistence;

public class NullObjectPatternExample {

	private Persistence persistence = EmptyPersistence.getInstance();

	public void persistence(Persistence persistence) {
		this.persistence = persistence;
	}

	public String get() {
		return persistence.get();
	}

	public void insert(String value) {
		persistence.insert(value);
	}

	public void update(String value) {
		persistence.update(value);
	}

	public void delete() {
		persistence.delete();
	}
}

Persistence.java

package com.maixuanviet.patterns.behavioral.nullobject.persistence;

public interface Persistence {

	void delete();

	void insert(String value);

	void update(String value);

	String get();
}

3. Lợi ích của Null Object Pattern là gì?

  • Code trở nên đơn giản hơn, giảm bớt các điều kiện kiểm tra.
  • Giảm khả năng xảy ra lỗi Null Pointer Exception.

4. Sử dụng Null Object Pattern khi nào?

  • Đối phó với các đối tượng null.
  • Thay vì kiểm tra đối tượng null, chúng ta xác định hành vi null hoặc hành vi được gọi nhưng không làm gì.
  • Cung cấp hành vi, giá trị mặc định trong trường hợp dữ liệu không có sẵn.
  • Tạo các đối tượng để thử nghiệm, trong trường hợp tài nguyên thật không có sẵn.

Lời kết:

Một trong những cách dễ nhất để quản lý null là không sử dụng nó. Tuy nhiên, các lập trình viên Java có thói quen khởi tạo dữ liệu (thuộc tính & biến) thành null. Điều này khiến chúng ta có nguy cơ sử dụng một phương thức trên đối tượng null, do đó gây ra ngoại lệ tại thời điểm run-time. Thông thường, nên sử dụng empty để khởi tạo dữ liệu, chẳng hạn empty collection, empty string , … Ngoài ra, trong một số trường hợp chúng ta nên áp dụng Null Object Pattern hay Optional trong Java 8 để giúp chương trình gọn ràng hơn, dễ đọc hơn và ít bug hơn.