空对象模式


受 GOF(Gang of Four,四人组)《设计模式》一书的影响,让人误以为设计模式只有 23 种,其实不然,除了《设计模式》介绍的 23 种设计模式外,还有很多经典的设计模式,例如我们本文将要介绍的空对象模式。

定义

在空对象模式(Null Object Pattern)中,一个空对象取代 NULL 对象实例的检查。

英文定义如下:

Provide an object as a surrogate for the lack of an object of a given type. The Null Object provides intelligent do nothing behavior, hiding the details from its collaborators.
意思是:为缺少的对象提供一个默认的无意义对象,用来避免 Null 对象的产生。

简单来说,就是用一个空对象,来取代程序中的 Null 值判断,从而让调用者可以直接使用对象,而无需关心对象是否为 Null。

例如,在没用空对象模式之前,要正确的获取以下值:

String isocode = user.getAddress().getCountry().getIsocode().toUpperCase();

它的实现代码如下:

if (user != null) {(img)
    Address address = user.getAddress();
    if (address != null) {
        Country country = address.getCountry();
        if (country != null) {
            String isocode = country.getIsocode();
            if (isocode != null) {
                isocode = isocode.toUpperCase();
            }
        }
    }
}

非空判断已经多到令我们崩溃了,如果属性中还有更多的对象,那 Null 值判断就更多了,为了解决这个问题,就要使用本文将要介绍的空对象模式了。

组成角色

空对象模式包含如下角色:

  • 抽象对象(Abstract Object)角色:声明统一的对象行为(属性和方法);
  • 具体对象(Concrete Object)角色:确实存在的具体对象,程序中的非 Null 对象;
  • 空对象(Null Object)角色:非具体存在的对象,Null 对象;
  • 对象工厂(Object Factory)角色:根据传递的标识得到相关类的工厂类,返回值可以是具体对象或 Null 对象。

角色之间的 UML 关系图如下:

观察者模式代码实现

抽象对象

/**
 * 抽象对象
 */
abstract class AbstractObject {
    String name;
    abstract String getName();
    abstract boolean isNull();
}

具体对象

/**
 * 具体对象
 */
class ConcreteObject extends AbstractObject {
    public ConcreteObject(final String name) {
        this.name = name;
    }
    @Override
    String getName() {
        return this.name;
    }
    @Override
    boolean isNull() {
        return false;
    }
}

空对象

/**
 * 空对象
 */
class NullObject extends AbstractObject {
    @Override
    String getName() {
        return "Not Available in Customer Database";
    }
    @Override
    boolean isNull() {
        return true;
    }
}

对象工厂

/**
 * 对象生成工厂
 */
class ObjectFactory {
    public static AbstractObject creator(final String name) {
        AbstractObject result = null;
        switch (name) {
            case "Java":
                result = new ConcreteObject("Java");
                break;
            case "SQL":
                result = new ConcreteObject("SQL");
                break;
            default:
                result = new NullObject();
                break;
        }
        return result;
    }
}

程序执行结果如下:

Java

Not Available in Customer Database

SQL

从以上的代码可以看出,其中 getName () 为所有对象需要执行的公共方法,如果没使用空对象模式的情况下,每次在调用 getName () 之前,我们需要先判空再使用,而如果使用的是空对象模式的话,则可以直接使用(该方法)。

优缺点

空对象模式的优点:

  • 省去代码中对 Null 值的判断和检查;
  • 让代码显的更加优雅和可读性更高;
  • 让系统更加稳定,避免程序抛出 NullPointerException 异常。
    空对象模式的缺点:
  • 因为增加了更多的类信息,从而使系统更复杂。

应用场景

JDK 8 中的 Optional 对象使用的就是空对象模式,避免空指针的异常,同时又能写出优雅而简洁的 Java 代码。
Optional 类中有以下几个重要的方法:

  • ofNullable () 方法:为指定的值创建一个 Optional, 如果指定的值为 null,则返回一个空的 Optional 对象;
  • orElse () 方法:如果有值则将其返回,否则返回指定的其它值;
  • map () 方法:如果创建的 Optional 中的值存在,对该值执行提供的 Function 函数调用;
  • flagMap () 方法:如果创建的 Optional 中的值存在,就对该值执行提供的 Function 函数调用,返回一个 Optional 类型的值,否则就返回一个空的 Optional 对象。

小贴士:很多人可能对 “对该值执行提供的 Function 函数调用” 这句话不太理解,它的意思是说,例如下面代码:
Optional.ofNullable(concreteUser).flatMap(u -> u.getAddress())
其中 “(u -> u.getAddress ())” 这部分代码就是 “该值执行提供的 Function 函数”。

接下来我们就是用 Optional 对象,优雅的实现判空操作,优雅的实现文章开头 4 层令人崩溃的 Null 值判断,实现代码如下。

用户类

/**
 * 用户类
 **/
class User {
    public User(Address address) {
        this.address = address;
    }
    private Address address;
    public Optional<Address> getAddress() {
        return Optional.ofNullable(address);
    }
    public void setAddress(Address address) {
        this.address = address;
    }
}

地址类

/**
 * 地址类
 **/
class Address {
    public Address(Country country) {
        this.country = country;
    }
    private Country country;
    public Optional<Country> getCountry() {
        return Optional.ofNullable(country);
    }
    public void setCountry(Country country) {
        this.country = country;
    }
}

国际编码类

/**
 * 国际编码类
 **/
class Country {
    public Country(String isocode) {
        this.isocode = isocode;
    }
    private String isocode;
    public String getIsocode() {
        return isocode;
    }
    public void setIsocode(String isocode) {
        this.isocode = isocode;
    }
}

客户端调用

public class Client {
    public static void main(final String[] args) {
        // JDK 8 Optional 对象判空示例
        // 具体对象
        User concreteUser = new User(new Address(new Country("china")));
        // 空对象
        User nullUser = new User(null);
        // 具体对象编码获取
        String concreteIsocode = Optional.ofNullable(concreteUser)
                .flatMap(u -> u.getAddress())
                .flatMap(a -> a.getCountry())
                .map(c -> c.getIsocode())
                .orElse("暂无").toUpperCase();
        // 空对象编码获取
        String nullIsocode = Optional.ofNullable(nullUser)
                .flatMap(u -> u.getAddress())
                .flatMap(a -> a.getCountry())
                .map(c -> c.getIsocode())
                .orElse("暂无").toUpperCase();
        System.out.println("Concrete User:" + concreteIsocode);
        System.out.println("Null User:" + nullIsocode);
    }
}

程序直接结果如下:

Concrete User:CHINA

Null User:暂无

使用实例

以生活中场景为例,例如,在一个小商店里,售货员可以根据商品的编码,得到商品的具体名称和价格等信息,实现代码如下。

抽象商品类

/**
 * 抽象商品类
 **/
abstract class AbstractGoods {
    public String isbn; // 商品编码
    public String name;
    public Double price;
    public abstract void show();
}

具体商品类

/**
 * 具体商品
 **/
class ConcreteGoods extends AbstractGoods {
    public ConcreteGoods(String isbn, String name, Double price) {
        this.isbn = isbn;
        this.name = name;
        this.price = price;
    }
    @Override
    public void show() {
        System.out.println("商品名:" + this.name + ",价格:" + this.price + "元");
    }
}

空商品类

/**
 * 空商品类
 **/
class NullGoods extends AbstractGoods {
    @Override
    public void show() {
        System.out.println("商品信息暂无!");
    }
}

商品工厂

/**
 * 商品查询工厂
 **/
class GoodsFactory {
    public static AbstractGoods find(final String isbn) {
        AbstractGoods result = null;
        switch (isbn) {
            case "001":
                result = new ConcreteGoods("001", "Java面试全解析", 69.0);
                break;
            case "002":
                result = new ConcreteGoods("002", "MySQL面试金典", 19.0);
                break;
            default:
                result = new NullGoods();
                break;
        }
        return result;
    }
}

客户端调用

public class Client {
    public static void main(final String[] args) {
        final AbstractGoods goods1 = GoodsFactory.find("001");
        final AbstractGoods goods2 = GoodsFactory.find("003");
        goods1.show();
        goods2.show();
    }
}

程序执行结果如下:

商品名:Java 面试全解析,价格:69.0 元

商品信息暂无!

总结

在 Java 语言中,解决 NullPointerException 异常的常见方法是使用空对象模式,空对象模式可以省去代码中对 Null 值的判断,从而使代码更加的简洁和优雅。在 JDK 8 之后,Java API 给我们提供了 Optional 类,使用它可以优雅且有效的,规避空对象产生 NullPointerException 的问题。


Author: Re:0
Reprint policy: All articles in this blog are used except for special statements CC BY 4.0 reprint policy. If reproduced, please indicate source Re:0 !
 Previous
策略模式 策略模式
定义策略模式(Strategy Pattern:Define a family of algorithms,encapsulate each one,and make them interchangeable.)中文解释为:定义一组算法,然
2022-03-17
Next 
状态模式 状态模式
定义状态模式(Allow an object to alter its behavior when its internal state changes.The object will appear to change its class.
2022-03-12
  TOC