空对象模式
空对象模式(Null Object Pattern)是一种设计模式,用于解决在某些情况下不需要实例化具体的对象,而是返回一个“空”对象,这样可以简化代码、避免 NullPointerException 和提高程序的可读性和维护性。简单来说,”空对象模式“就是本该返回None值或抛出异常时,返回一个符合正常结果接口的特制“空类型对象”来代替,以此免去调用方法的错误处理工作。
来看一个例子,现有多份问卷调查的得分记录,全部为字符串格式,存放在一个列表中:
data = ['bruce_liu 98', 'mike 100', 'invalid-data', 'roland $invalid_points', ...]
正常的得分记录是{username} {points}格式,但你会发现, 有些数据明显不符合规范(比如invalid-data)。现在统计合格(大于等于80)的得分记录总数,代码如下:
QUALIFIED_POINTS = 80
class CreateUserPointError(Exception):
"""创建得分纪录失败时抛出"""
class UserPoint:
"""用户得分记录"""
def __init__(self, username, points):
self.username = username
self.points = points
def is_qualified(self):
"""返回得分是否合格"""
return self.points >= QUALIFIED_POINTS
def make_userpoint(point_string):
"""从字符串初始化一条得分记录
:param point_string: 形如 "piglei 1" 的表示得分记录的字符串
:return: UserPoint 对象
:raises: 当输入数据不合法时返回 CreateUserPointError
"""
try:
username, points = point_string.split()
points = int(points)
except ValueError:
raise CreateUserPointError('input must follow pattern "{username} {points}"')
if points < 0:
raise CreateUserPointError('points can not be negative')
return UserPoint(username=username, points=points)
def calc_qualified_count(points_data):
"""计算得分合格的总人数
:param points_data: 字符串格式的用户得分列表
"""
result = 0
for point_string in points_data:
try:
point_obj = make_userpoint(point_string)
except CreateUserPointError:
pass
else:
result += point_obj.is_qualified()
return result
data = [
'bruce_liu 98',
'nobody 61',
'cotton 83',
'invalid_data',
'roland $invalid_points',
'alfred -3',
]
print(calc_qualified_count(data))
#输出结果
# 2
在上面的代码里,因为输入数据可能不符合要求,所以make_userpoint()方法在解析输入数据、创建UserPoint对象的过程中,可能会抛出CreateUserPointError异常来通知调用方。
因此,每当调用make_userpoint()时, 都必须加上try/except语句来捕获异常。假如引入“空对象模式”,上面的异常处理逻辑可以完全消失, 代码如下:
QUALIFIED_POINTS = 80
class UserPoint:
"""用户得分记录"""
def __init__(self, username, points):
self.username = username
self.points = points
def is_qualified(self):
"""返回得分是否合格"""
return self.points >= QUALIFIED_POINTS
class NullUserPoint:
"""一个空的用户得分记录"""
username = ''
points = 0
def is_qualified(self):
return False
def make_userpoint(point_string):
"""从字符串初始化一条得分记录
:param point_string: 形如 "piglei 1" 的表示得分记录的字符串
:return: 如果输入合法,返回 UserPoint 对象,否则返回 NullUserPoint
"""
try:
username, points = point_string.split()
points = int(points)
except ValueError:
return NullUserPoint()
if points < 0:
return NullUserPoint()
return UserPoint(username=username, points=points)
def count_qualified(points_data):
"""计算得分合格的总人数
:param points_data: 字符串格式的用户得分列表
"""
return sum(make_userpoint(s).is_qualified() for s in points_data)
data = [
'bruce_liu 98',
'nobody 61',
'cotton 83',
'invalid_data',
'roland $invalid_points',
'alfred -3',
]
print(calc_qualified_count(data))
在这个代码里,定义了一个代表“空得分记录”的新类型:NullUserPoint,每当make_userpoint()接收到无效的输入,执行失败时,就会返回一个NullUserPoint对象。这样修改后,count_qualified()就不再需要处理任何异常了:
def count_qualified(points_data):
"""计算得分合格的总人数
:param points_data: 字符串格式的用户得分列表
"""
return sum(make_userpoint(s).is_qualified() for s in points_data)
这里的make_userpoint()总是会返回一个符合要求的对象(UserPoint()或NullUserPoint()),同前面unset命令的故事一样,“空对象模式”也是一种转换设计观念以避免错误处理的技巧。当函数进入边界情况时,“空对象模式”不再抛出错误