背景
空对象模式(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)) ); } }
|
用户类
一个简单的用户信息,后续业务将会根据 age
和
expectPrice
属性对展示的商品进行过滤
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); } 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; listener = (EvictionListener<K, V>) DiscardingListener.INSTANCE; }
|
在通过 Builder
进行实例化中,真正创建的
ConcurrentLinkedHashMap
对象会根据默认值设置一个通知队列
pendingNotifications
1 2 3 4 5 6 7 8
| private ConcurrentLinkedHashMap(Builder<K, V> builder) { 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
| static final Queue<?> DISCARDING_QUEUE = new DiscardingQueue();
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)