同样的结论也适用于封印类。在为一个简单且稳定的领域建模时,封装性原则并不一定会为我们带来好处,甚至还可能让客户端更加难以使用简单的领域内容。
当然,这并不说封装性原则是错误的,而是说成本和收益之间的权衡有时候不是那么明显。我们可以自己判断什么时候可以从中获得好处,什么时候会给我们造成阻碍。在选择是公开还是隐藏实现时,我们必须清楚封装性原则的好处和成本。通常,封装性是有好处的,但在为简单的领域建模时,封装性的好处可能会大打折扣。
如果一个类型,比如 Shape,限定了接口和实现类,我们就可以更放心地把它转成 Circle,因为 Shape 将 Circle 列为它的已知子类型之一。就像记录类是一种更透明的类,求和类型是一种更透明的多态性。这就是为什么求和类型和乘积类型会如此频繁一起出现。它们都代表了透明性和抽象性之间的某种折衷,因此,适合使用其中一个类型的地方也适合使用另一个类型。乘积和类型通常被称为 代数数据类型。
穷举
像 Shape 这样的封印类限定了一系列子类型,有助于程序员和编译器作出推断,而如果没有这些信息,我们就做不到。其他工具也可以利用这些信息。Javadoc 工具在生成的文档页面中列出了封印类允许的子类型。
Java SE 14 引入了一种有限定的 模式匹配,在未来会进一步扩展。第一个版本允许我们在 instanceof 中使用类型模式:
if (shape instanceof Circle c) { // 编译器已经为我们将 shape 转成 Circle 类型,并赋值给 c
System.out.printf(“Circle of radius %d%n”, c.radius());}
这离在 switch 中使用类型模式已经不远了。(Java SE 15 还不支持,但很快就会出现。) 到了那个时候,我们可以使用 switch 表达式(case 后面直接是类型)来计算一个形状的面积,如下所示:
float area = switch (shape) {
case Circle c -> Math.PI * c.radius() * c.radius();
case Rectangle r -> Math.abs((r.upperRight().y() - r.lowerLeft().y())