空对象模式练习

空对象模式练习——日志器、可选服务与空节点

按《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)不判空
  • 客户代码:用 PushNotifierNullNotifier 各调一次 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() -> Nodeget_right() -> Node
  • 实现 RealNode:构造时接收 value、left、right;get_left/right 若传入为 None 则内部用 NullNode(或单例)代替,保证不返回 None。
  • 实现 NullNodeget_value() 返回 Noneget_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 判断空节点。

自检问题

  1. 空对象None 相比,对调用方有什么好处?
    调用方不用写 if x is not None,统一按接口调用;空对象实现同一接口,多态替换,不会出现“对 None 调方法”的 AttributeError。

  2. 空对象的方法应该怎样实现?
    要么什么都不做(无操作),要么返回安全、合理的默认值(如空字符串、0、空列表、None);不应抛异常,也不应依赖外部资源。

  3. 什么时候不适合用空对象?
    当“没有”必须被显式区分并做不同逻辑时(如“未登录”必须跳转登录页),用 None 或 Optional 更合适,调用方必须分支;空对象适合“没有就当什么都不做/用默认值”的场景。

做完以上三道练习,再对照《空对象模式》文档中的示例,对空对象模式会掌握得比较扎实。建议每道题先自己写一遍,再对照参考答案和验证要点检查。

发表评论