python设计模式–空对象模式(Null Object Pattern)

Python 设计模式——空对象模式(Null Object Pattern)详解

本文面向零基础新手,从“用 None 的麻烦”到“用空对象代替 null”,从日志、依赖到树节点,全方位讲解空对象模式。


一、什么是空对象模式?

1.1 通俗理解

空对象模式是一种行为型设计模式,核心思想是:

当某个依赖可能“没有”时,不要用 None(或 null)表示“没有”,而是提供一个和正常对象实现同一接口的“空对象”;这个空对象的方法要么什么都不做,要么返回安全的默认值。这样调用方不需要到处写 if x is not None,直接调接口即可,代码更简洁、更安全。

可以这样类比:

  • 不用空对象:你有一个“日志器”,有时没配置日志器就传 None。每次打日志都要写 if logger is not None: logger.log(...),否则可能报错。代码里到处都是“判空”。
  • 用空对象:提供一个“空日志器”(NullLogger),和真实日志器实现同一接口,但 log 方法什么都不做。没配置时传 NullLogger 而不是 None,打日志时直接 logger.log(...),不需要 if。“没有”也是一种对象,只是行为是“无操作”。

所以,空对象模式解决的是:用“空对象”代替 null/None,让调用方不必反复判空,同时保持接口一致、避免 None 导致的 AttributeError。

1.2 为什么需要空对象模式?

场景一:避免到处判空

若依赖可能是 None,每个使用处都要写 if obj is not None: obj.do_something(),代码重复且容易漏写。用空对象后,obj 永远非 None,直接 obj.do_something(),空对象内部什么都不做即可。

场景二:接口一致、多态

调用方只依赖“接口”(如 Logger),不关心是真实实现还是“空实现”;真实和空都实现同一接口,可以多态替换。测试或未配置时注入空对象,业务代码不用改。

场景三:安全默认值

例如“没有父节点”时返回 NullNode 而不是 None,调用 parent.get_name() 不会报错,NullNode.get_name() 返回空字符串或固定值;遍历树时不用每一层都判空。

1.3 适用场景(什么时候用?)

场景 说明
依赖可选、经常要判空 如可选日志、可选回调、可选策略,希望调用方不写 if obj。
需要“无操作”或安全默认 没有真实对象时,提供“什么都不做”或“返回默认值”的替代品。
接口统一、便于测试 测试时注入空对象代替真实依赖,业务代码不分支。

简单记忆可能没有的对象同接口的“空实现”代替 None,省判空、保安全、接口一致


二、空对象模式的结构(三个角色)

角色 说明
抽象类型(接口) 定义客户依赖的方法(如 log、handle、get_name);真实对象和空对象都实现该接口。
真实对象(Real Object) 正常实现,有实际行为。
空对象(Null Object) 实现同一接口,但方法体为空操作(什么都不做)或返回安全默认值(如空字符串、0、空列表);不抛异常、不依赖外部资源。

关系可以理解为:

  • 客户只依赖接口,拿到的可能是真实对象,也可能是空对象;客户不判空,统一调用接口。
  • 空对象和真实对象可替换:没有真实对象时,把空对象注入进去即可。

三、示例一:日志器(真实 vs 空)

需求:业务代码依赖“日志器”接口写日志;有时未配置日志器,希望不报错、也不到处写 if logger。提供真实 Logger 和 NullLogger,未配置时用 NullLogger。

3.1 接口 + 真实实现 + 空对象

from abc import ABC, abstractmethod

class Logger(ABC):
    @abstractmethod
    def log(self, message: str):
        pass

class FileLogger(Logger):
    def __init__(self, path: str):
        self.path = path

    def log(self, message: str):
        print(f"  [写入文件 {self.path}] {message}")

class NullLogger(Logger):
    """空对象:实现同一接口,但什么都不做"""
    def log(self, message: str):
        pass  # 无操作

3.2 使用:不判空

def do_work(logger: Logger):
    logger.log("开始处理")   # 不写 if logger
    # ... 业务逻辑 ...
    logger.log("处理完成")

# 有日志器时
do_work(FileLogger("app.log"))

# 未配置日志器时,注入空对象
do_work(NullLogger())

要点:do_work 不关心 logger 是 FileLogger 还是 NullLogger,也不写 if logger is not None;NullLogger 的 log 是空操作,不会报错。


四、示例二:可选依赖(服务 / 回调)

需求:某个服务可能未注入(如邮件服务、支付回调);调用方希望统一写 service.send(...)callback.on_success(),未配置时“什么都不做”。用空对象代替 None。

class EmailService(ABC):
    @abstractmethod
    def send(self, to: str, subject: str, body: str):
        pass

class RealEmailService(EmailService):
    def send(self, to: str, subject: str, body: str):
        print(f"  [发送邮件] 给 {to}: {subject}")

class NullEmailService(EmailService):
    def send(self, to: str, subject: str, body: str):
        pass

# 依赖注入时:没配置就注 NullEmailService
email_service = NullEmailService()  # 或 RealEmailService()
email_service.send("user@example.com", "标题", "内容")  # 从不判空

五、示例三:树节点(没有子节点时用空节点)

需求:树节点有左子、右子;没有子节点时若用 None,每次访问都要 if node is not None。改用“空节点”(NullNode),和真实节点同一接口,get_value 返回默认值、get_left/get_right 返回自己或另一个空节点,这样遍历时不用判空。

class Node(ABC):
    @abstractmethod
    def get_value(self):
        pass

    @abstractmethod
    def get_left(self) -> "Node":
        pass

    @abstractmethod
    def get_right(self) -> "Node":
        pass

class RealNode(Node):
    def __init__(self, value, left: Node = None, right: Node = None):
        self._value = value
        self._left = left or NullNode()
        self._right = right or NullNode()

    def get_value(self):
        return self._value

    def get_left(self) -> Node:
        return self._left

    def get_right(self) -> Node:
        return self._right

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

# 遍历时不用判空
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())

root = RealNode(1, RealNode(2), RealNode(3))
print(sum_tree(root))  # 6

要点:RealNode 用 NullNode() 代替 None 表示“没有子节点”;NullNode 的 get_left/get_right 返回自己,sum_tree 遇到“值为 None”就返回 0,无需写 if node is None。


六、示例四:返回空集合 / 空迭代(常见变体)

有时“没有结果”不返回 None,而返回空列表、空字典,调用方可以统一 for 或 .get,不用判空。这也是空对象思想的一种体现。

def get_user_roles(user_id: int):
    # 若没有角色,返回空列表而不是 None
    return []  # 或 roles_list,调用方: for r in get_user_roles(uid) 不用 if result

七、空对象模式 vs 判空 / Optional

方式 做法 缺点 / 优点
直接用 None 没有就传 None,调用处 if x is not None 容易漏判、代码重复。
Optional + 判空 类型标注 Optional[T],调用前检查 类型更清晰,但仍要写判空。
空对象 没有就传同接口的“空实现” 调用方不判空、接口一致;多一个空对象类。

简单记忆:空对象 = 用“空实现”代替 null同一接口调用方不判空


八、常见问题与注意点

8.1 空对象要不要单例?

若空对象无状态、到处可共用,可以做成单例(如 NullNode、NullLogger),省内存。若有状态或希望每次 new 也可以,看需求。

8.2 什么时候不该用空对象?

若“没有”必须被明确区分(如“用户未登录”和“用户已登录但无权限”),用 None 或 Optional 更合适,调用方必须分支处理。空对象适合“没有时就当什么都不做/用默认值”的场景。

8.3 空对象方法要不要返回 None?

看接口约定。若接口返回 str,空对象可返回 “”;若返回 list,可返回 [];若返回 None 也是合理默认,就返回 None。原则是不抛异常、调用方能安全使用返回值


九、小结

  • 空对象模式:用与真实对象同一接口空对象代替 null/None;空对象方法无操作或返回安全默认值,使调用方不必判空、代码更简洁、避免 None 导致的错误。
  • 三个角色:抽象类型(接口)、真实对象、空对象(空实现或默认值)。
  • 典型用途:可选日志、可选服务/回调、树/图的空节点、可选策略等。
  • 实现要点:空对象实现完整接口;方法体为空或返回默认值;调用方只依赖接口,不写 if obj is not None。

建议先写“Logger + NullLogger”,再试“可选服务”或“树空节点”,体会“不判空、接口一致”,这样对空对象模式会掌握得比较扎实。

发表评论