贫瘠之地

华北无浪漫,死海扬起帆
多少个夜晚,独自望着天

0%

空对象模式 - Null Object

背景

空对象模式(Null Object Pattern),使用空对象的行为(空实现、校验等)来代替对 Null 值的判断;空对象并不是在检查空值,而是通过对象的行为实现不进行任何动作或者校验的效果,以此对调用方隐藏更多的实现细节

目的 向上层隐藏更多的实现细节,加强系统的稳定性,减少判空判断

现实世界类比 在现实世界中也很难表达 ”空“ 这个概念,往往会使用 ”空盒子“、”空间“ 来进行表达,类比在代码中就是使用表现空概念的对象,而不是判空 obj == null 来实现对空的判断

实践

模拟这样一个场景,对用户展示一些商品信息,其中要根据用户的属性对商品进行过滤

问题

对于最终过滤后的商品,如果无法满足一定数量,例如商品数量 < 3,就会从通用商品池中选择一定数量的商品进行补位

这样的需求在代码流程中如何设计,如果在过滤流程、VO 转换流程等阶段来实现,就会让逻辑看起来不太顺畅

这时就可以考虑使用空对象模式

实现

全部商品信息

先定义出一个商品信息集合,当然这些商品信息可以通过接口、数据库系统等来获取

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Data
@AllArgsConstructor
public class GoodsInfo {

private String name;

private Integer num;

private Pair<Integer, Integer> ageRange;

private BigDecimal price;

public static List<GoodsInfo> getAllGoodsInfo() {
return Arrays.asList(
new GoodsInfo("洗衣机", 10, new Pair<>(20, 50), BigDecimal.valueOf(2000.0)),
new GoodsInfo("笔记本电脑", 21, new Pair<>(15, 50), BigDecimal.valueOf(4999.0)),
new GoodsInfo("扫地机器人", 5, new Pair<>(25, 55), BigDecimal.valueOf(2500.0)),
new GoodsInfo("厨具", 30, new Pair<>(30, 60), BigDecimal.valueOf(89.0)),
new GoodsInfo("文具", 100, new Pair<>(8, 30), BigDecimal.valueOf(30.0)),
new GoodsInfo("空调", 6, new Pair<>(30, 60), BigDecimal.valueOf(5100.0)),
new GoodsInfo("儿童玩具", 40, new Pair<>(10, 50), BigDecimal.valueOf(100.0))
);
}
}

用户类

一个简单的用户信息,后续业务将会根据 ageexpectPrice 属性对展示的商品进行过滤

1
2
3
4
5
6
7
8
9
10
@Data
public class User {

private String name;

private int age;

private Pair<BigDecimal, BigDecimal> expectPrice;

}

过滤规则接口及实现

提供过滤规则接口

1
2
3
public interface GoodsFilterHandler {
List<GoodsInfo> filter(List<GoodsInfo> goodsInfos, User user);
}

根据年龄进行过滤实现

根据商品信息上的年龄和用户的年龄进行

1
2
3
4
5
6
7
8
public class AgeFilter implements GoodsFilterHandler {
@Override
public List<GoodsInfo> filter(final List<GoodsInfo> goodsInfos, final User user) {
return goodsInfos.stream().filter(
goodsInfo -> goodsInfo.getAgeRange().getKey() <= user.getAge() && goodsInfo.getAgeRange().getValue() >= user
.getAge()).collect(Collectors.toList());
}
}

根据期望价格区间进行过滤实现

1
2
3
4
5
6
7
8
public class ExpectPriceFilter implements GoodsFilterHandler {
@Override
public List<GoodsInfo> filter(final List<GoodsInfo> goodsInfos, final User user) {
return goodsInfos.stream().filter(
goodsInfo -> user.getExpectPrice().getKey().compareTo(goodsInfo.getPrice()) <= 0
&& user.getExpectPrice().getValue().compareTo(goodsInfo.getPrice()) >= 0).collect(Collectors.toList());
}
}

过滤流程模板方法及实现

该抽象类主要进行两部分操作:

  • 实现类注册过滤器实现
  • filter 方法根据注册的过滤器实现对结果集进行过滤,其中在最后通过工厂方法对结果集生成不同的 Converter 实现类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public abstract class AbstractGoodsFilter {

protected abstract List<GoodsFilterHandler> goodsFilterHandlers();

public List<String> filter(User user) {
List<GoodsFilterHandler> goodsFilterHandlers = goodsFilterHandlers();
List<GoodsInfo> curGoods = GoodsInfo.getAllGoodsInfo();
for (GoodsFilterHandler handler : goodsFilterHandlers) {
curGoods = handler.filter(curGoods, user);
}
// 生成不同的 Converter 实现类
return GoodsInfoConverterFactory.buildConverter(curGoods).buildVO();
}
}

Common 实现

注册了过滤规则的两个实现

1
2
3
4
5
6
public class CommonGoodsFilter extends AbstractGoodsFilter {
@Override
protected List<GoodsFilterHandler> goodsFilterHandlers() {
return Arrays.asList(new AgeFilter(), new ExpectPriceFilter());
}
}

转换器工厂与实现

  • buildConverter 方法根据结果集生成不同的实现类
  • CommonConverter 没有行为
  • LackConverter 即空对象模式的实现,会在返回的结果集上补充模拟商品(好吧,感觉这个例子有点牵强)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
public class GoodsInfoConverterFactory {

private static final int DEFAULT_GOODS_INFO_LACK_SIZE = 3;

private GoodsInfoConverterFactory() {
}

public static GoodsInfoConverter buildConverter(List<GoodsInfo> goodsInfos) {
if (CollectionUtil.isEmpty(goodsInfos) || goodsInfos.size() < DEFAULT_GOODS_INFO_LACK_SIZE) {
return new LackConverter(goodsInfos);
}
return new CommonConverter(goodsInfos);
}

public abstract static class GoodsInfoConverter {
protected List<GoodsInfo> result;

public List<String> buildVO() {
List<GoodsInfo> res = convert();
return res.stream().map(GoodsInfo::getName).collect(Collectors.toList());
}

protected abstract List<GoodsInfo> convert();

protected GoodsInfoConverter(final List<GoodsInfo> result) {
this.result = result;
}
}

private static class CommonConverter extends GoodsInfoConverter {
public CommonConverter(final List<GoodsInfo> result) {
super(result);
}

@Override
protected List<GoodsInfo> convert() {
return this.result;
}
}

private static class LackConverter extends GoodsInfoConverter {
public LackConverter(final List<GoodsInfo> result) {
super(result);
}

@Override
protected List<GoodsInfo> convert() {
final ArrayList<GoodsInfo> res = new ArrayList<>(result);
for (int i = result.size(); i < DEFAULT_GOODS_INFO_LACK_SIZE; i++) {
res.add(new GoodsInfo("模拟商品", 1, null, null));
}
return res;
}
}
}

使用

任意构造一个用户,经过过滤器返回其展示的优先级高的商品

其中根据过滤规则,用户 1 无法满足 >= 3 的条件,所以使用了模拟商品进行补位

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public static void main(String[] args) {
User user1 = new User();
user1.setName("张三");
user1.setAge(20);
user1.setExpectPrice(new Pair<>(BigDecimal.valueOf(10), BigDecimal.valueOf(500)));

User user2 = new User();
user2.setName("李四");
user2.setAge(50);
user2.setExpectPrice(new Pair<>(BigDecimal.valueOf(2000), BigDecimal.valueOf(7000)));

CommonGoodsFilter filter = new CommonGoodsFilter();

// [文具, 儿童玩具, 模拟商品]
List<String> res1 = filter.filter(user1);

// [洗衣机, 笔记本电脑, 扫地机器人, 空调]
List<String> res2 = filter.filter(user2);
}

调整规则

可以看到最终空对象的业务逻辑(这里是判断 size 小于阈值,也可以理解为一种空行为)和整个过滤逻辑无关,业务逻辑中也不需要进行判空(工厂方法除外),对于上游 AbstractGoodsFilter 而言,它只是在执行过滤逻辑,而具体结果集并不关心,结果集的转换由空对象实现来实现

如果产品需求进行变动,当 size 小于 3 时直接抛出异常,则整个大的业务逻辑都不需要变动,只需要改变空对象的实现

1
2
3
4
5
6
7
8
9
10
private static class LackConverter extends GoodsInfoConverter {
public LackConverter(final List<GoodsInfo> result) {
super(result);
}

@Override
protected List<GoodsInfo> convert() {
throw new IllegalStateException("推荐商品属性 size < " + DEFAULT_GOODS_INFO_LACK_SIZE);
}
}

实际应用

上面的例子在使用空对象实现来进行特殊的业务流程,还可以使用空对象模式来对默认行为进行空实现,这样可以减少上游调用的判空代码

例如 Google 的 ConcurrentLinkedHashMap

在实例化过程中(使用 ConcurrentLinkedHashMap.Builder 对象),listener 参数的默认值就是一个固定的实现

1
2
3
4
5
6
7
8
public Builder() {
capacity = -1;
weigher = Weighers.entrySingleton();
initialCapacity = DEFAULT_INITIAL_CAPACITY;
concurrencyLevel = DEFAULT_CONCURRENCY_LEVEL;
// 默认实现 DiscardingListener.INSTANCE
listener = (EvictionListener<K, V>) DiscardingListener.INSTANCE;
}

在通过 Builder 进行实例化中,真正创建的 ConcurrentLinkedHashMap 对象会根据默认值设置一个通知队列 pendingNotifications

1
2
3
4
5
6
7
8
private ConcurrentLinkedHashMap(Builder<K, V> builder) {
// ...
// The notification queue and listener
listener = builder.listener;
pendingNotifications = (listener == DiscardingListener.INSTANCE)
? (Queue<Node<K, V>>) DISCARDING_QUEUE
: new ConcurrentLinkedQueue<Node<K, V>>();
}

如果是默认值则会使用 DISCARDING_QUEUE

DISCARDING_QUEUE 就是一种空实现,会丢弃所有的通知(即不通知)

1
2
3
4
5
6
7
8
9
10
11
12
/** A queue that discards all entries. */
static final Queue<?> DISCARDING_QUEUE = new DiscardingQueue();

/** A queue that discards all additions and is always empty. */
static final class DiscardingQueue extends AbstractQueue<Object> {
@Override public boolean add(Object e) { return true; }
@Override public boolean offer(Object e) { return true; }
@Override public Object poll() { return null; }
@Override public Object peek() { return null; }
@Override public int size() { return 0; }
@Override public Iterator<Object> iterator() { return emptyList().iterator(); }
}

总结

空对象模式的优点:

  • 它可以加强系统的稳固性,能有有效地防止空指针报错对整个系统的影响,使系统更加稳定
  • 它能够实现对空对象情况的定制化的控制,能够掌握处理空对象的主动权
  • 它并不依靠 Client 来保证整个系统的稳定运行
  • 它通过 isNull==null 的替换,显得更加优雅,更加易懂

参考

空对象模式_百度百科 (baidu.com)