【问题标题】:Java Records and Null Object Pattern?Java 记录和空对象模式?
【发布时间】:2020-10-29 03:01:31
【问题描述】:

有没有办法用 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);
}

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

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

【问题讨论】:

  • (一个)记录的目的是不将 null 作为有效值,所以这个问题没有任何意义。
  • 是这样吗?这就是我正在寻找的那种论证,但openjdk.java.net/jeps/359 根本没有提到这一点。我在不同的代码库中有很多值对象,记录可能很有用,但它们的局限性使它变得很尴尬。
  • 也许你可以举一个“空”记录似乎是必要的例子?
  • 既然您将NULL_ID 声明为static final,就不存在并发问题了。条件NULL_OBJECT != null 只能在安全的类初始化器中评估为false。但这不是“空对象”设计模式。 Null Object 设计模式应该防止客户端代码遇到null 引用相关的问题。但是您的recordid() 方法仍然可能返回null,就像record 允许null 一样。
  • 旁注:使用 Eclipse 的 ejc,第一个代码块是有效的 - 直到有人 fixed my bug

标签: java java-14 null-object-pattern java-record


【解决方案1】:

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

代码中的基本错误

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

空对象与空对象

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

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

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

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

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

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

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

但是,在使用记录为可能返回 null(即可能返回“不适用”、“无值”或“未找到”的 API)的 API 时,经常发生的情况是调用者希望将其视为已知的方便对象

例如,如果我总是大写并修剪学生姓名,并且某些 id 已经映射到“不再注册”并且这显示为已映射到空字符串,那么它对调用者来说真的很方便希望 对于这个特定的用例,未找到的应该被视为空字符串。幸运的是,Map API 迎合了这一点:

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

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

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

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

您想要的大概是一个快速、不可变、易于访问且具有空字符串的 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) 是真的。

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

【讨论】:

  • 感谢您的反馈(我已修复缺失的最终结果)。然而我真的不想讨论模式本身。有趣的是技术限制(这可能会影响一般记录的有用程度)。我可能会将示例更改为更合理一些,这只是我能想象的最简洁的事情。
  • @atamanroman,我鼓励更改示例。 Null 对象实现默认行为。记录通常没有行为。两者似乎不太可能重叠。
【解决方案2】:

不,Java 14 中当前的记录定义无法实现您想要的。每种记录类型都有一个规范的构造函数,无论是隐式定义还是显式定义。每个非规范构造函数都必须从调用此记录类型的另一个构造函数开始。这基本上意味着,对任何其他构造函数的调用肯定会导致对规范构造函数的调用。 [8.10.4 Record Constructor Declarations in Java 14]

如果这个规范的构造函数进行参数验证(它应该这样做,因为它是公共的),你的选择是有限的。您要么遵循已经提到的建议/解决方法之一,要么只允许您的用户通过界面访问 API。如果您选择最后一种方法,则必须从记录类型中删除参数验证并将其放入接口中,如下所示:

public interface Id {
    Id NULL_ID = new IdImpl(null);

    String id();

    static Id newIdFrom(String id) {
        Objects.requireNonNull(id);
        return new IdImpl(id);
    }
}

record IdImpl(String id) implements Id {}

我不知道你的用例,所以这可能不是你的选择。但同样,你想要的现在是不可能的。

关于Java 15,我只能找到JavaDoc for Records in Java 15,似乎没有改变。我找不到实际的规范,JavaDoc中指向它的链接导致404,所以他们可能已经放宽了规则,因为有些人complained about them

【讨论】:

  • 好吧,为什么投反对票?如果我的回答有误,我会删除它。
猜你喜欢
  • 1970-01-01
  • 2018-07-24
  • 1970-01-01
  • 2013-02-20
  • 1970-01-01
  • 1970-01-01
  • 2015-11-20
  • 2018-05-11
  • 1970-01-01
相关资源
最近更新 更多