空对象模式与代理实现

在软件开发过程中,经常会遇到系统崩溃或类似的错误,这些错误往往是由于令人沮丧的NullPointerException引起的。为了保护软件免受此类问题的影响并使其更安全,通常会在代码中添加检查,以防止调用null变量。这导致了一个简单的方法,如:

public String getProjectManagerName() {
    return getProject().getManager().getName();
}

变成了这样:

public String getProjectManagerName() {
    Project project = getProject();
    if (project == null)
        return "";

    Manager manager = project.getManager();
    if (manager == null)
        return "";
    return manager.getName();
}

这种方法在只需要在一两个地方使用时效果很好,但如果需要在所有软件中重复使用,它会通过添加不必要的代码,显著降低其可读性和可扩展性。此外,这种类型的空逻辑无法为新代码提供保护,因此如果程序员忘记包含它,相同的问题可能会再次发生。

这个问题非常重要,以至于在更现代的语言如Groovy中引入了安全引用操作符(?.)。使用这个构造,前面的方法可以安全地重写如下:

public String getProjectManagerName() {
    return getProject()?.getManager()?.getName();
}

空对象模式如何解决这个问题

虽然Java语言没有提供安全引用操作符,但仍然可以通过使用所谓的空对象模式来实现相同的结果。根据Wikipedia的定义:

“空对象是一个具有定义的中性(“null”)行为的对象。”

换句话说,一个给定类的空对象与该类兼容(通过扩展它或实现一个公共接口),并在没有更有意义的行为的情况下提供默认的(“null”)行为。例如,前面代码片段中涉及的类Project和Manager的空对象版本可以如下实现:

public class NullProject extends Project {
    public Manager getManager() {
        return new NullManager();
    }
}

public class NullManager extends Manager {
    public String getName() {
        return "";
    }
}

请注意,空对象必须始终返回另一个空对象,以保持调用序列的安全。通过这种方式(假设getProject()方法在应该返回null时返回NullProject的实例),getProjectManagerName()的第一个版本再次变得安全,消除了第二个版本中引入的所有检查的需要。

在看来,尽管这种解决方案有效,并且允许编写更干净的代码,但它有一个主要缺陷:它迫使为领域模型中的每个类实现、维护和测试空对象版本。而且,它并没有以更微妙的方式为新代码提供保护:如果程序员在领域对象中添加了新方法,忘记了在其空对象版本中覆盖它,将面临NullPointerException的风险。

通过代理实现空对象模式

为了消除这些问题,最好通过代理动态生成空对象类,而不是静态实现它们。为了探索这个最后的解决方案,实现了一个名为NullObjectProxy的类,它附加在本文中,以及一个测试类,它更好地说明了如何使用它。这个代理的核心当然是它的invoke()方法,它定义了在拦截对空对象的调用时执行的操作。

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    String methodName = method.getName();
    if (methodName.equals("getNullClass") && (args == null || args.length == 0))
        return clazz;
    // 它将hashCode和equals方法委托给代理类
    if (methodName.equals("hashCode") && (args == null || args.length == 0))
        return clazz.hashCode();
    if (methodName.equals("equals") && args.length == 1 && args[0] instanceof NullObject)
        return ((NullObject)args[0]).getNullClass() == clazz;
    // 它检查是否为此方法调用定义了特殊返回值
    Method mockedMethod = getMockedMethod(method);
    if (mockedMethod != null)
        return mockedMethodValuesMap.get(mockedMethod);    
    Class returnedType = method.getReturnType();
    if (returnedType == Void.TYPE)
        return null;
    // 如果返回类型是接口
    if (returnedType.isInterface())
        return nullObjectOf(returnedType);
    // 它检查是否为此返回类型定义了默认值
    try {
        return NullObjectProxy.class.getMethod("null" + returnedType.getSimpleName() + "Value").invoke(NullObjectProxy.class);
    } catch (Exception e) { }
    // 它尝试实例化给定返回类型的一个对象,通过调用其空构造函数(如果有的话)
    try {
        return returnedType.newInstance();
    } catch (Exception e) { }
    // 如果所有前面的策略都失败了,为了避免ClassCastException,它只能返回null
    return null;
}

此类允许动态实例化一个代理,该代理为任何给定的接口实现空对象模式,通过将要模拟的接口的类传递给静态构造函数:

public static  T nullObjectOf(Class clazz);

这个代理保证了任何调用序列的安全性,因为每个方法调用都返回(如果可能的话)另一个空对象,该对象反过来模拟方法本身返回的类型。通过这种方式,要使原始的非检查实现的getProjectManagerName()方法安全,只需从getProject()调用返回nullObjectOf(Project.class)即可。

如上所述,当被调用方法返回的声明类型不是接口时,空对象代理无法生成另一个空对象。在这种情况下,代理尝试返回一个有意义的值,应用两种策略。首先,它检查是否有与请求兼容的预定义默认返回值,特别是对于任何Number返回0,对于boolean返回false,对于char返回空格,对于String和Date分别返回空字符串和表示当前系统时间的Date。然后,如果没有上述情况适用,它尝试实例化给定返回类型的一个对象,通过调用其空构造函数(如果有的话)。最后,如果这两种策略都失败了,为了避免ClassCastException,它唯一能做的就是返回null。

setMockedMethod(Collection.class, "isEmpty", true);
沪ICP备2024098111号-1
上海秋旦网络科技中心:上海市奉贤区金大公路8218号1幢 联系电话:17898875485