简短的回答是“您必须这样做,因为这就是 API 的编写方式”。当然,您可能真正要问的是为什么要这样编写 API。我认为这实际上相当于两个(相关)问题。一个是关于方法名称,以及该方法的作用,一个是关于实际的 API 设计。
一般来说,在计算中,重用现有问题的解决方案通常是有益的。 (知道什么时候这样做有好处是一门艺术,但在这两种情况下,这显然是一种好处。)这种 API 设计以两种不同的方式重用现有解决方案来解决众所周知的问题,从而使其成为对于以前遇到过这些解决方案的程序员来说更容易理解和使用。
场景图
为了大局,请考虑一般用户界面的整体结构。有一个大型容器(想想 JavaFX 中的场景根),其中包含各种 UI 组件。其中一些可能是简单的控件,例如按钮和标签等,其中一些可能是其他容器,这些容器又包含各种 UI 组件。当然,其中一些也可能是容器,等等。如果不强加某种结构,这可能会变得复杂且难以使用。
为了理解结构,我们将其抽象化。有一个根节点,它具有零个或多个其他节点的集合。每个节点都有零个或多个其他节点的集合,依此类推。这是计算中非常著名的抽象结构,称为“树”,基本上每个计算机程序员(无论他们使用哪种编程语言)都熟悉它。因此,如果我们将其视为树结构,因为我们已经熟悉它,我们可以使复杂性更容易处理。为了将其视为树结构,我们对树状方面使用标准名称。每个节点(根节点除外)都有一个“父节点”(它所属的容器):因此您会在 Node 类中找到一个方法(“节点”是树结构中的另一个术语),称为getParent(),它允许访问父节点(包含当前节点的容器)。类似地,包含其他节点的 UI 元素(节点)有一个名为 getChildren() 的方法,该方法返回当前节点中包含的所有节点的集合。 Oracle JavaFX 教程中有一个关于 Scene graph 的部分对此进行了描述,其中包含许多漂亮的图表和相关代码。
所以,简而言之,之所以会有一个名为getChildren()的方法,是因为我们将UI中所有事物的集合视为一个树形结构,而这个方法名称正是描述了该方法在上下文中的作用所有 UI 元素的集合都是一棵树。术语“节点”、“父”和“子”对于有一点经验的程序员来说是一眼就能认出来的,并帮助他们理解整体结构并使用它。他们基本上可以立即推断出getChildren() 返回该容器中立即包含的所有 UI 元素的列表(在您的示例中,该容器是 StackPane)。
Panes的API设计(如StackPane)
要考虑 API 设计,请考虑在操纵 StackPane 中包含的内容方面您可能想做的所有事情。正如您所观察到的,您可能希望向StackPane 添加一个新的UI 元素(“节点”),因此您可能需要一个名为add(...) 的方法来接受Node。但是您可能还需要或想做很多其他事情:
- 从
StackPane 中删除一个节点
- 将整个节点集合添加到
StackPane
- 从
StackPane 中删除整个节点集合
- 从(清除)
StackPane 中删除所有节点
- 由于堆栈窗格中节点的顺序很重要(它定义了 z 顺序,即如果节点重叠,它定义了哪些在前面绘制,哪些在后面绘制),您可能希望在特定位置插入一个节点(在某物前面,但在其他东西后面)
- 删除“后面”、前面或某个指定位置的节点
- 还有很多很多其他的
所以StackPane 类的设计者可以编写所有这些方法,但有更好的方法。
如果您考虑实现StackPane,您需要一些方法来跟踪它包含的节点(UI 元素)。这些节点的顺序很重要,所以我们必须跟踪它,并且我们需要有一个很好的方法来定义上面列出的所有功能。 StackPane 类(或其超类,Pane)的作者可以从头开始构建一个数据结构来执行此操作,但标准库中已经存在一个。一个包含特定类型对象集合并跟踪它们的顺序的数据结构称为List1,List 接口是每个Java 程序员熟悉。
因此,如果您考虑StackPane 的实际实现,您可以在堆栈窗格本身中为上面列出的所有功能定义方法。这最终会看起来像
public class StackPane {
private final List<Node> children = new ArrayList<>(); // or some other list implementation...
public void add(Node node) {
children.add(node);
}
public boolean remove(Node node) {
return children.remove(node);
}
public void add(int index, Node node) {
children.add(index, node);
}
public boolean remove(int index) {
return children.remove(index);
}
public void addAll(Collection<Node> nodes) {
children.addAll(nodes);
}
// lots more methods like this...
// lots of layout code omitted...
}
我想你明白了。所有这些代码实际上并没有做任何事情。它只是调用已经定义的行为。因此,只需提供对列表本身的访问权限,就可以为StackPane 的用户提供完全相同的功能,而不是这个臃肿的 API2:
public class StackPane {
private final List<Node> children = new ArrayList<>();
public List<Node> getChildren() {
return children ;
}
// layout code omitted...
}
现在类变得不那么臃肿了,API 也不那么臃肿了,另外,用户收到了一个List,正如前面所指出的,这是一种非常知名的对象类型。 JavaFX 的新程序员,假设他们有一些(任何)Java 经验,将已经熟悉List 的 API,并且不需要学习太多新的东西就可以使用它。所以程序员现在可以做(代码布局非常规并插入程序员的想法):
StackPane pane = new StackPane();
Button button = new Button("OK");
pane.getChildren()
// oooh, a List, I know how those work
.add(button);
List<Label> lotsOfLabels = Arrays.asList(new Label("One"), new Label("Two"), new Label("Three"));
pane.getChildren()
// still a list, I know more things I can do here:
.addAll(lotsOfLabels);
pane.getChildren().remove(button);
pane.getChildren().clear();
我们有一个快乐的程序员,他立即知道 API,并且在第一次接触到在 JavaFX 中管理场景图时立即高效,因此,我们有一个快乐的老板,他看到编程团队的生产力很高。
因此,总而言之,通过简单地公开子节点的List,API 可以公开操作StackPane 内容所需的所有功能,而无需向StackPane 类添加大量方法列表并且以利用每个 Java 程序员现有知识的方式来做。
脚注
- 实际上,
Panes 需要的功能比List 中定义的要多一点:他们需要一种方便的方法来了解列表何时更改,通过添加或删除节点(因为窗格需要重新计算其布局)有时候是这样的)。所以 JavaFX 团队定义了List 的子接口,称为ObservableList,它具有List 的所有功能,并添加了“观察”列表的功能。
- JavaFX 团队在这里必须认真考虑一件事:all 由
List 定义的功能是否适合子节点的集合?如果List接口定义了一些在这里真的没有意义的方法,那么使用这个实现可能是个坏主意,而第一个代码块中建议的更臃肿的API实际上可能是一个更好的选择。李>