走进 Gang of Four 设计模式:空对象模式

📅 2026/7/21 8:17:53
走进 Gang of Four 设计模式:空对象模式
走进 Gang of Four 设计模式空对象模式说明不仅告诉你怎么用更告诉你为什么这样设计前置知识Java 面向对象基础接口、多态、继承⚠关于 GoF 归属空对象模式并非 GoF 《设计模式》一书中收录的 23 种模式之一。它由 Bobby Woolf 于 1996 年首次提出是特例模式Special Case Pattern的一个特例。虽然在分类上不属于经典 GoF但在工业界的广泛应用使其成为每个开发者必须掌握的行为型模式。 设计模式分类总览GoF 23 种设计模式可按两个维度交叉分类类/对象处理方式 ×创建型/结构型/行为型目的。维度创建型Creational结构型Structural行为型Behavioral类Class通过继承复用Factory Method工厂方法Adapter类适配器Interpreter解释器Template Method模板方法对象Object通过组合/聚合复用Abstract Factory抽象工厂Builder建造者Prototype原型Singleton单例Adapter对象适配器Bridge桥接Composite组合Decorator装饰Facade外观Flyweight享元Proxy代理Chain of Resp.责任链Command命令Iterator迭代器Mediator中介者Memento备忘录Observer观察者State状态Strategy策略Visitor访问者本文档聚焦的Null Object空对象不属于经典 GoF 23但在面向对象设计中应用广泛常被归为补充行为型模式。 目录模式概述 · 15 问深度分析框架源码实战分析Java Collections.emptyList()SLF4J / Log4j NOPLoggerSpring StringValueResolver 空对象深度追问Why / How / Trade-off / Evolution / Modern Practice总结参考文献1. 模式概述 · 15 问深度分析Q1: 为什么需要这个模式它解决了什么问题在传统的编程习惯中当一个方法无法返回有效数据时通常会返回null。这就迫使调用方必须在使用返回值前进行if (obj ! null)的检查。它核心解决的是代码臃肿Null 检查泛滥和健壮性问题防止因疏忽导致的NullPointerException。Q2: 如果不用这个模式会有什么缺陷代码可读性极差业务逻辑被淹没在层层叠叠的if (xxx ! null)判空语句中。容易引发线上故障只要有一处开发人员忘记判空程序就会直接抛出 NPE 崩溃。职责错配调用方本应该只关注怎么使用这个对象却不得不承担检查这个对象是否存在的额外责任。// ❌ 代码中充斥着防御性判空publicvoidprocessOrder(Orderorder){if(order!null){Useruserorder.getUser();if(user!null){Addressaddruser.getAddress();if(addr!null){// 真正想写的业务逻辑Stringcityaddr.getCity();System.out.println(city);}}}}Q3: 核心思想是什么一句话如何概括核心思想以行为代替判断用一个不做任何事的合法的具体实现对象来代表空状态。一句话概括用无作为的实例消灭null带来的防御性编程。Q4: 包含哪些角色每个角色的职责是什么空对象模式包含以下 3 个核心角色抽象组件Abstract Operation / Interface定义业务行为的接口或抽象类。真实对象Real Object实现了抽象组件代表真正拥有业务逻辑的具体类。空对象Null Object同样实现了抽象组件但其内部方法全部是空实现例如方法体为空、返回0、返回空字符串或返回假值。Q5: 它们之间如何协作调用流程是怎样的工厂/数据源判定客户端请求某个对象数据源或工厂进行查找。对象组装如果找到了返回RealObject如果没找到不返回null而是返回预先创建好的NullObject。无感知调用客户端拿到对象后直接调用接口方法。如果是RealObject则正常执行如果是NullObject方法被安全地空执行流程继续走下去没有异常抛出。Q6: 为什么要这样设计每个角色存在的意义是什么抽象组件存在的意义在于统一类型体系。只有大家拥有相同的接口客户端才能实现多态调用。真实对象负责聚焦并实现真正的核心业务不需要夹杂任何处理我不存在的边缘逻辑。空对象存在的意义是充当安全的替身。它承载了系统的缺省行为将原本需要客户端写if判断的逻辑内聚到了自己空实现的方法体中。Q7: 为什么使用接口、抽象类而不是其他方式因为该模式的核心支撑是面向对象的多态性Polymorphism。接口/抽象类提供了类型一致性的契约。如果不通过继承体系客户端就无法用统一的语法object.request()去同时对待有效对象和无效对象。如果不采用多态就只能用if-else分支或者反射这又回到了老路上。Q8: 这种设计符合哪些面向对象原则里氏替换原则 (LSP)完美符合。NullObject作为子类可以完全透明地替换AbstractObject出现的任何地方而不会破坏客户端原有程序的正确性。开闭原则 (OCP)当需要定义新的空行为或扩展真实业务时只需增加新的实现类无需修改客户端的调用逻辑。单一职责原则 (SRP)客户端只负责调用方法执行业务而判断对象是否存在并提供默认行为的职责交给了空对象或工厂。Q9: 它有哪些优点和局限性会带来哪些额外成本优点消除if (xxx ! null)代码极其整洁、优雅。客户端零风险彻底杜绝 NPE。局限性/额外成本类膨胀每一个接口都需要额外配备一个配套的NullXXX类。掩盖错误有时null的出现代表了系统底层的异常故障。如果盲目使用空对象将其吞掉可能会让本该暴露的 Bug 隐蔽得更深增加排查问题的难度。Q10: 它最适合哪些场景哪些场景不应该使用最适合场景对象存在默认的什么都不做的行为如日志打印器当未配置时用NullLogger不打印任何东西。集合/系统需要遍历或链式调用如数据结构中的树节点、链表尾部的边界哨兵。不应该使用场景当缺失数据属于严重异常时比如获取用户银行余额如果获取不到应该报错而不是返回一个NullBalance显示余额为0这会导致业务逻辑大乱。Q11: 它与容易混淆的其他设计模式有什么区别和联系与状态模式State/ 策略模式Strategy联系空对象模式可以看作是状态模式的一种特例代表空这一状态或者策略模式的一种特例代表空行为这一策略。区别状态/策略模式更侧重于运行时不同行为的动态切换空对象模式单纯为了解决不存在的问题。与单例模式Singleton联系因为空对象不保存任何状态所有属性都是一致的所以空对象通常会结合单例模式实现全局只保留一个实例节约内存。Q12: 框架中有哪些典型应用Java 集合框架Collections.emptyList()、Collections.emptyMap()。当方法需要返回集合但没有数据时返回空集合而不是null这样调用方直接for循环也不会报错。Spring FrameworkStringValueResolver的实现中包含不进行任何替换的NullSource。Log4j / SLF4J当某个类没有配置特定的 Logger 时系统会提供一个NOPLogger所有info()、debug()方法全部为空防止报空指针。Q13: 如何从零实现一个最小可运行版本// 1. 抽象组件interfaceLog{voidlog(Stringmessage);}// 2. 真实对象classConsoleLogimplementsLog{publicvoidlog(Stringmessage){System.out.println(日志输出: message);}}// 3. 空对象 (结合单例模式)classNullLogimplementsLog{privatestaticfinalNullLoginstancenewNullLog();privateNullLog(){}publicstaticNullLoggetInstance(){returninstance;}Overridepublicvoidlog(Stringmessage){// 什么都不做优雅地保持沉默}}// 4. 工厂classLogFactory{publicstaticLoggetLog(booleanisConfigured){returnisConfigured?newConsoleLog():NullLog.getInstance();}}// 5. 客户端——没有任何判空publicclassMain{publicstaticvoidmain(String[]args){LoglogLogFactory.getLog(false);log.log(测试数据);// 安全运行无输出不抛异常}}Q14: 如何在现有项目中识别可以使用该模式的地方代码里有大量的if (service ! null)、if (user ! null)保护性代码。很多if (obj null)后面的else块里做的事情是return;、return 或者什么都不做。在回调函数或监听器列表中如果某个事件没有监听器可以用一个空的监听器实例占位从而直接遍历调用。Q15: 如果重新设计这个模式在今天会有哪些改进Java 8 的OptionalOptional本身就是一个通用的、泛型的空对象容器。userService.findUser(id).ifPresent(User::upgradePrivilege)免去了手动写NullUser类的麻烦。函数式编程Lambda传一个空的 Lambda 表达式() - {}即可充当空对象消除了类膨胀。SpringAutowired(required false)OptionalSpring 允许注入OptionalMyService如果容器中没有该 Bean自动注入Optional.empty()完全省去了手写NullMyService。2. 框架源码实战分析2.1 Java Collections.emptyList()核心矛盾方法返回null调用方用for-each遍历时抛 NPE。而Collections.emptyList()返回了一个预先定义好的空 List 实例。核心源码// java.util.CollectionsSuppressWarnings(rawtypes)publicstaticfinalListEMPTY_LISTnewEmptyList();SuppressWarnings(unchecked)publicstaticfinalTListTemptyList(){return(ListT)EMPTY_LIST;}// 空对象的具体实现类内部类privatestaticclassEmptyListEextendsAbstractListEimplementsRandomAccess,Serializable{privatestaticfinallongserialVersionUID8808830602431671569L;publicintsize(){return0;}// 大小永远为 0publicbooleanisEmpty(){returntrue;}publicbooleancontains(Objectobj){returnfalse;}publicIteratorEiterator(){returnCollections.emptyIterator();// 返回空迭代器}publicEget(intindex){thrownewIndexOutOfBoundsException(Index: index);// 不是 NPE是明确异常}publicvoidadd(intindex,Eelement){thrownewUnsupportedOperationException();// 不可变}}逐步分析类型对齐多态EmptyList继承自AbstractList实现了List接口。对于调用方而言它就是一个纯正的List。安全的for循环for (Object obj : list)→list.iterator()→emptyIterator()→hasNext()永远返回false→ 循环安全结束。不用该模式ListString list null;→for(String s : list)→ 试图在null上调用iterator()→NPE 崩溃。使用该模式ListString list Collections.emptyList();→for(String s : list)→ 空迭代器 →安全结束。2.2 SLF4J NOPLogger 源码分析核心矛盾项目中只引入slf4j-api.jar却忘记引入具体实现如 Logback时SLF4J 不会报空指针而是用NOPLogger让程序继续静默运行。核心源码// org.slf4j.helpers.NOPLoggerpublicclassNOPLoggerextendsMarkerIgnoringBase{publicstaticfinalNOPLoggerNOP_LOGGERnewNOPLogger();protectedNOPLogger(){}// 所有状态判断返回 falsepublicfinalbooleanisTraceEnabled(){returnfalse;}publicfinalbooleanisDebugEnabled(){returnfalse;}publicfinalbooleanisInfoEnabled(){returnfalse;}publicfinalbooleanisWarnEnabled(){returnfalse;}publicfinalbooleanisErrorEnabled(){returnfalse;}// 所有业务打印方法——空方法体publicfinalvoidtrace(Stringmsg){}publicfinalvoiddebug(Stringmsg){}publicfinalvoidinfo(Stringmsg){}publicfinalvoidwarn(Stringmsg){}publicfinalvoiderror(Stringmsg){}}逐步分析统一类型NOPLogger实现了org.slf4j.Logger接口和LogbackLogger拥有完全一致的接口。LoggerFactory当找不到配置时拒绝返回null而是返回NOPLogger.NOP_LOGGER。流程logger.info(开始执行)→ 调用NOPLogger.info()→ 空方法体 → 安全返回不中断线上业务。2.3 Spring StringValueResolver 空对象Spring 在处理属性占位符时使用StringValueResolver接口。当没有配置解析器时使用空对象直接返回原字符串。// Spring 的 StringValueResolver 接口FunctionalInterfacepublicinterfaceStringValueResolver{NullableStringresolveStringValue(StringstrVal);}// 空对象不做任何处理直接返回publicclassNoOpStringValueResolverimplementsStringValueResolver{OverridepublicStringresolveStringValue(StringstrVal){returnstrVal;// 原样返回不做任何处理}}逐步分析核心引擎在解析文本时经过一个ListStringValueResolver处理器链。如果不引入空对象引擎每前进一步都要写if (resolver ! null) { value resolver.resolveStringValue(value); }。引入NoOpStringValueResolver后直接调用resolver.resolveStringValue(value)零判空。3. 深度追问3.1 Why为什么解决哪个根本矛盾空对象模式解决的根本矛盾是类型系统的静态安全契约与动态数据缺失之间的矛盾。null的出现本质上是对类型系统的破坏——它是一个伪装成该类型、却不具备该类型任何行为能力的幽灵。调用方明明只想消费行为现在却不得不充当质检员用大量if (obj ! null)来防御类型系统的失效。空对象模式通过引入一个合法的无作为实例让类型契约在任何时候都保持完好。3.2 How怎么做通过哪些关系和交互机制解决通过**“多态替换和静态占位”**机制来解决问题。对象关系空对象与真实对象是平级的兄弟关系共同继承自同一个抽象组件。交互机制配置/创建期容器或工厂在初始化时如果发现依赖缺失或查询无果不会将引用设为null而是将空对象的单例注入给调用方。运行/调用期客户端调用component.execute()利用多态动态绑定如果背后是空对象则进入预设的空方法体调用方完全无感知。3.3 Trade-off代价牺牲了什么换取什么换取了收益极高的代码可读性与健壮性消灭防御性判空。职责内聚统一缺失状态下的默认行为到空对象内部。牺牲了代价类膨胀每个业务接口需要配套一个NullObject接口变化时两个类要同步修改。对核心故障的掩盖最大代价null有时是重要警报。盲目套用空对象会隐蔽地吞掉真正的 Bug。3.4 Evolution演化如何演化而来第一阶段早期程序大量返回null或特定错误码调用方稍有疏忽就崩溃。第二阶段哨兵对象单链表末尾的NullNode、树的NullNode等。第三阶段经典空对象模式由 Woolf 在 1996 年正式总结为独立模式。衍生变体智能空对象Smart Null Object被调用时利用动态代理记录调用链路用于开发阶段的调试。特例模式Special Case Pattern由 Martin Fowler 提出空对象模式是它的特例。特例还可代表未知、“匿名”、欠费用户等。3.5 Modern Practice现代实践在今天是否被取代被取代/升级的场景OptionalT语言级取代Java 8 中Optional本身就是一个通用的、泛型的空对象包装器。Lambda 表达式行为级取代空 Lambda() - {}可直接充当空对象消除了类膨胀。依然无法替代的场景微服务治理熔断降级Spring Cloud 的Fallback类本质上就是分布式系统中的空对象模式无法被Optional取代。大型框架的底层设计SLF4J 的NOPLogger、Spring 的各种NoOp处理器——在构建高度抽象的底层框架时手动设计高内聚的空对象依然是最干净利落的选择。4. 总结核心要点空对象模式用无作为的实例消灭null用多态代替条件判断与单例模式结合空对象通常是单例因为无状态可共享适合缺失数据在业务上是正常情况的场景不适合缺失数据代表严重异常的场景三个工业级应用Collections.emptyList()空集合迭代安全、NOPLogger日志静默降级、NoOpStringValueResolver空处理链核心代价可能掩盖真正的问题Bug 被静默吞掉一句话空对象模式就是既然没东西可用那就给个啥也不干的替身——用无作为的实例代替null消除防御性编程让业务代码回归纯粹。5. 参考文献[1] WOOLF B. The Null Object Pattern[J]. PLoP, 1996.[2] FOWLER M. Patterns of Enterprise Application Architecture[M]. Boston: Addison-Wesley, 2002.[3] BLOCH J. Effective Java[M]. 3rd ed. Boston: Addison-Wesley, 2018.[4] Oracle. Java Collections Framework[EB/OL]. https://docs.oracle.com/javase/tutorial/collections/, 2024.[5] SLF4J. NOPLogger Source Code[EB/OL]. https://www.slf4j.org/, 2024.