首页 > 解决方案 > Java 记录和空对象模式?

问题描述

有没有办法用 Java 记录做空对象?对于课程,我会这样做:

public class Id {

  public static final Id NULL_ID = new Id();

  private String id;

  public Id(String id) {
    this.id = Objects.requireNonNull(id);
  }

  private Id() {}
}

但这不起作用,因为每个构造函数都需要通过规范的 ( Id(String id) 构造函数,而我不能只调用super()来绕过不变量。

public record Id(String id) {
  public static final Id NULL_ID = null; // how?

  public Id {
    Objects.requireNonNull(id);
    // ...
  }
}

现在我解决这个问题

public Id {
  if (NULL_OBJECT != null)
    Objects.requireNonNull(id);
}

但这感觉不对,并且容易出现并发问题。

我还没有发现很多关于唱片背后的设计理念的讨论,这可能已经讨论过了。如果是这样保持简单,那是可以理解的,但感觉很尴尬,我已经在小样本中多次遇到过这个问题。

标签: javajava-14null-object-patternjava-record

解决方案


我强烈建议你停止使用这种模式。它有各种各样的问题:

代码中的基本错误

您的 NULL_ID 字段final显然不应该是。

空对象与空对象

有两个概念看起来相似甚至相同,但实际上并非如此。

未知/未找到/不适用的概念。例如:

Map<String, Id> studentIdToName = ...;
String name = studentIdToName.get("foo");

name如果"foo"不在地图中应该是什么?

没那么快——在你回答之前:嗯,也许""——这会导致各种各样的问题。如果您编写的代码错误地认为使用的 id 肯定在此映射中,那么这是一个既成事实:此代码存在错误。时期。我们现在所能做的就是确保尽可能“好”地处理这个错误。

并且说namenull这里,严格来说是优越的:该错误现在将是明确的,堆栈跟踪指向有问题的代码。没有堆栈跟踪并不是无错误代码的证明——根本不是。如果此代码返回空字符串,然后将电子邮件发送到空白邮件地址,其正文包含名称应为空字符串的正文,这比抛出 NPE 的代码差得多。

对于这样的值(未找到/未知/不适用),java 中没有什么null比值更重要。

null但是,在使用记录为可能返回的 API(即,可能返回“不适用”、“无值”或“未找到”的API)时,经常发生的情况是调用者想要处理这个与已知的方便对象相同

例如,如果我总是大写并修剪学生姓名,并且某些 id 已经映射到“不再注册”并且这显示为已映射到空字符串,那么调用者想要这个未找到的特定用例应该被视为空字符串。幸运的是,MapAPI 迎合了这一点:

String name = map.getOrDefault(key, "").toUpperCase().trim();
if (name.isEmpty()) return;
// do stuff here, knowing all is well.

作为 API 设计者,您应该提供的关键工具是一个空对象

空对象应该是方便的。你的不是。

因此,既然我们已经确定“空对象”不是您想要的,但“空对象”是很好的,请注意它们应该很方便。调用者已经决定了他们想要的一些特定行为;他们明确选择了这一点。他们不想那样仍然必须处理需要特殊处理的唯一值,并且具有字段为 null 的Id实例id无法通过便利测试。

您想要的大概是一个快速、不可变、易于访问并且有一个空字符串作为 id 的 Id。不为空。喜欢"",还是喜欢List.of()"".length()工作,并返回 0。someListIHave.retainAll(List.of())工作,并清除列表。这就是工作上的便利。这是危险的便利(因为,如果您不期望虚拟对象具有某些众所周知的行为,则当场不出错可以隐藏错误),但这就是调用者必须明确选择加入它的原因,例如使用getOrDefault(k, THE_DUMMY).

那么,你应该在这里写什么呢?

简单的:

private static final Id EMPTY = new Id("");

您可能需要 EMPTY 值具有某些特定行为。例如,有时您希望 EMPTY 对象也具有它是唯一的属性;没有其他 Id 实例可以被认为与它相等。

您可以通过两种方式解决该问题:

  1. 隐藏的布尔值。
  2. 通过使用 EMPTY 作为显式标识。

我认为“隐藏的布尔值”很明显。私有构造函数可以初始化为 true 的私有布尔字段,并且所有可公开访问的构造函数都设置为 false。

使用 EMPTY 作为身份有点棘手。例如,它看起来像这样:

@Override public boolean equals(Object other) {
    if (other == null || !other.getClass() == Id.class) return false;
    if (other == this) return true;
    if (other == EMPTY || this == EMPTY) return false;
    return ((Id) other).id.equals(this.id);
}

这里,EMPTY.equals(new Id(""))其实是假的,却EMPTY.equals(EMPTY)是真的。

如果这就是您希望它工作的方式(有问题,但在某些用例中,规定空对象是唯一的是有意义的),那就去做吧。


推荐阅读