空对象模式练习——日志器、可选服务与空节点
按《Python 设计模式——空对象模式》的建议,通过三道练习巩固:① 日志器与空日志器;② 可选通知服务与空实现;③ 树节点与空节点。每步都有完整可运行代码和验证要点。
练习一:日志器与空日志器
目的
体会用空对象代替 None:调用方不写 if logger is not None,统一写 logger.log(...);未配置日志器时注入 NullLogger,其 log 方法为空操作,不报错。
要求
- 定义 Logger 接口,有方法 log(message: str)。
- 实现 ConsoleLogger(真实对象):log 时用 print 输出到控制台。
- 实现 NullLogger(空对象):实现同一接口,log 方法什么都不做(pass 或只 return)。
- 写一个函数 do_task(logger: Logger):内部调用两次 logger.log(“…”),不写 if logger。
- 客户代码:先传 ConsoleLogger() 调用 do_task,验证有输出;再传 NullLogger() 调用 do_task,验证无输出且不报错。
参考答案
from abc import ABC, abstractmethod
class Logger(ABC):
@abstractmethod
def log(self, message: str):
pass
class ConsoleLogger(Logger):
def log(self, message: str):
print(f" [日志] {message}")
class NullLogger(Logger):
def log(self, message: str):
pass
def do_task(logger: Logger):
logger.log("任务开始")
logger.log("任务结束")
# 有日志器
do_task(ConsoleLogger())
# [日志] 任务开始
# [日志] 任务结束
# 未配置日志器,用空对象
do_task(NullLogger())
# 无输出,且不报错
验证要点
- 传 ConsoleLogger 时,控制台出现两行 [日志] …。
- 传 NullLogger 时,没有任何输出,且没有 AttributeError 或 NoneType 错误。
- 确认:do_task 内部没有
if logger is not None,始终直接调用 logger.log(…)。
练习二:可选通知服务与空实现
目的
某个“通知服务”可能未配置(如未开启推送);业务代码统一写 notifier.notify(user_id, msg),未配置时注入空通知器,notify 为空操作。
要求
- 定义 Notifier 接口,有方法 notify(user_id: str, message: str)。
- 实现 PushNotifier(真实对象):notify 时 print 一条“[推送] 给 user_id: message”。
- 实现 NullNotifier(空对象):notify 方法什么都不做。
- 写一个函数 send_alert(notifier: Notifier, user_id: str, msg: str):内部只调 notifier.notify(user_id, msg),不判空。
- 客户代码:用 PushNotifier 和 NullNotifier 各调一次 send_alert,验证前者有输出、后者无输出且不报错。
参考答案
from abc import ABC, abstractmethod
class Notifier(ABC):
@abstractmethod
def notify(self, user_id: str, message: str):
pass
class PushNotifier(Notifier):
def notify(self, user_id: str, message: str):
print(f" [推送] 给 {user_id}: {message}")
class NullNotifier(Notifier):
def notify(self, user_id: str, message: str):
pass
def send_alert(notifier: Notifier, user_id: str, msg: str):
notifier.notify(user_id, msg)
# 有通知器
send_alert(PushNotifier(), "u1", "您的订单已发货")
# [推送] 给 u1: 您的订单已发货
# 未配置通知器
send_alert(NullNotifier(), "u1", "您的订单已发货")
# 无输出,不报错
验证要点
- PushNotifier 时有一行推送输出;NullNotifier 时无输出、无异常。
- send_alert 内部没有 if notifier,只依赖 Notifier 接口。
练习三:树节点与空节点
目的
树节点有左子、右子;没有子节点时不用 None,而用空节点(NullNode),与真实节点同一接口,get_value 返回 None(或 0),get_left/get_right 返回自己(或单例空节点)。遍历时对“空节点”做统一处理,不必写 if node is None。
要求
- 定义 Node 接口:get_value()、get_left() -> Node、get_right() -> Node。
- 实现 RealNode:构造时接收 value、left、right;get_left/right 若传入为 None 则内部用 NullNode(或单例)代替,保证不返回 None。
- 实现 NullNode:get_value() 返回 None;get_left() 和 get_right() 返回自己(或同一单例),表示“没有子节点”。
- 写一个函数 sum_tree(node: Node) -> int:若 node.get_value() is None 则返回 0(视为空节点);否则返回 value + sum_tree(left) + sum_tree(right)。不写 if node is None,只根据 get_value() 判断。
- 客户代码:建一棵小树 root = RealNode(10, RealNode(5), RealNode(15)),求 sum_tree(root),验证结果为 30。
参考答案
from abc import ABC, abstractmethod
class Node(ABC):
@abstractmethod
def get_value(self):
pass
@abstractmethod
def get_left(self) -> "Node":
pass
@abstractmethod
def get_right(self) -> "Node":
pass
class NullNode(Node):
_instance = None
def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
def get_value(self):
return None
def get_left(self) -> Node:
return self
def get_right(self) -> Node:
return self
class RealNode(Node):
def __init__(self, value: int, left: Node = None, right: Node = None):
self._value = value
self._left = left if left is not None else NullNode()
self._right = right if right is not None else NullNode()
def get_value(self):
return self._value
def get_left(self) -> Node:
return self._left
def get_right(self) -> Node:
return self._right
def sum_tree(node: Node) -> int:
if node.get_value() is None:
return 0
return node.get_value() + sum_tree(node.get_left()) + sum_tree(node.get_right())
# 使用: 10
# /
# 5 15
root = RealNode(10, RealNode(5), RealNode(15))
print(sum_tree(root)) # 30
验证要点
- sum_tree(root) 得到 30(10+5+15)。
- sum_tree 里没有
if node is None,只有 if node.get_value() is None(用“空节点”的约定表示没有真实数据)。 - RealNode 的 left/right 为 None 时用 NullNode() 代替,保证 get_left/get_right 永不返回 None。
三步汇总与自检
| 练习 | 重点 | 关键点 |
|---|---|---|
| 一 | 日志器 + 空日志器 | 调用方不判空;NullLogger.log 为空操作;未配置时注入 NullLogger。 |
| 二 | 通知器 + 空通知器 | 同一接口;NullNotifier.notify 为空操作;可选依赖用空对象代替 None。 |
| 三 | 树节点 + 空节点 | 无子节点用 NullNode,get_left/get_right 不返回 None;遍历用 get_value() is None 判断空节点。 |
自检问题
-
空对象和 None 相比,对调用方有什么好处?
调用方不用写 if x is not None,统一按接口调用;空对象实现同一接口,多态替换,不会出现“对 None 调方法”的 AttributeError。 -
空对象的方法应该怎样实现?
要么什么都不做(无操作),要么返回安全、合理的默认值(如空字符串、0、空列表、None);不应抛异常,也不应依赖外部资源。 -
什么时候不适合用空对象?
当“没有”必须被显式区分并做不同逻辑时(如“未登录”必须跳转登录页),用 None 或 Optional 更合适,调用方必须分支;空对象适合“没有就当什么都不做/用默认值”的场景。
做完以上三道练习,再对照《空对象模式》文档中的示例,对空对象模式会掌握得比较扎实。建议每道题先自己写一遍,再对照参考答案和验证要点检查。