我强烈建议你停止使用这种模式。它有各种各样的问题:
代码中的基本错误
您的 NULL_ID 字段final
显然不应该是。
空对象与空对象
有两个概念看起来相似甚至相同,但实际上并非如此。
有未知/未找到/不适用的概念。例如:
Map<String, Id> studentIdToName = ...;
String name = studentIdToName.get("foo");
name
如果"foo"
不在地图中应该是什么?
没那么快——在你回答之前:嗯,也许""
——这会导致各种各样的问题。如果您编写的代码错误地认为使用的 id 肯定在此映射中,那么这是一个既成事实:此代码存在错误。时期。我们现在所能做的就是确保尽可能“好”地处理这个错误。
并且说name
在null
这里,严格来说是优越的:该错误现在将是明确的,堆栈跟踪指向有问题的代码。没有堆栈跟踪并不是无错误代码的证明——根本不是。如果此代码返回空字符串,然后将电子邮件发送到空白邮件地址,其正文包含名称应为空字符串的正文,这比抛出 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 设计者,您应该提供的关键工具是一个空对象。
空对象应该是方便的。你的不是。
因此,既然我们已经确定“空对象”不是您想要的,但“空对象”是很好的,请注意它们应该很方便。调用者已经决定了他们想要的一些特定行为;他们明确选择了这一点。他们不想那样仍然必须处理需要特殊处理的唯一值,并且具有字段为 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 实例可以被认为与它相等。
您可以通过两种方式解决该问题:
- 隐藏的布尔值。
- 通过使用 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)
是真的。
如果这就是您希望它工作的方式(有问题,但在某些用例中,规定空对象是唯一的是有意义的),那就去做吧。