在构建 JavaFX 控制器时,什么是好的编码实践?

What is good coding practice when it comes to structuring a JavaFX controller?

我是一名学习如何使用 JavaFX 的学生,我已经通过使用 SceneBuilder 和控制器获得了我的第一个 GUI class。然而,从我的角度来看,控制器中的代码结构看起来非常混乱和丑陋,因为我将每个事件处理程序都放在控制器的 initialize() 方法中。这使它看起来像这样:

@FXML
private void initialize() {
    dealtHandLabel.setText("Your cards will be shown here.");
    TextInputDialog userInput = new TextInputDialog();
    userInput.setTitle("How many cards?");
    userInput.setHeaderText("Enter how many cards you want to get.");
    userInput.setContentText("No. of cards:");

    //This makes it so that the button displays a hand of cards (of specified amount) when clicked
    dealHand.setOnAction(event -> {
        Optional<String> result = userInput.showAndWait();
        if(result.isPresent()) {
            int requestedAmount = Integer.parseInt(result.get());
            StringBuilder sb = new StringBuilder();
            cardHand = deck.dealHand(requestedAmount);
            cardHand.forEach((card) -> sb.append(card.getAsString()).append(" "));
            dealtHandLabel.setText(sb.toString());
        }
    });

    //This button uses lambdas and streams to display requested information (sum, heart cards, etc.)
    checkHand.setOnAction(event -> {
        int cardSum = cardHand.stream().mapToInt(card -> card.getFace()).sum();
        List<PlayingCard> spadeCards = cardHand.stream().filter((card) -> card.getSuit() == 'S').toList();
        List<PlayingCard> heartCards = cardHand.stream().filter((card) -> card.getSuit() == 'H').toList();
        List<PlayingCard> diamondCards = cardHand.stream().filter((card) -> card.getSuit() == 'D').toList();
        List<PlayingCard> clubCards = cardHand.stream().filter((card) -> card.getSuit() == 'C').toList();
        StringBuilder sb = new StringBuilder();
        heartCards.forEach((card) -> sb.append(card.getAsString()).append(" "));

        sumOfFacesField.setText(String.valueOf(cardSum));
        heartCardsField.setText(sb.toString());

        if(heartCards.size() >= 5 || diamondCards.size() >= 5 || spadeCards.size() >= 5 || clubCards.size() >= 5) {
            flushField.setText("Yes");
        }
        else {
            flushField.setText("No");
        }
        if(cardHand.stream().anyMatch((card) -> card.getAsString().equals("S12"))) {
            spadesQueenField.setText("Yes");
        }
        else {
            spadesQueenField.setText("No");
        }
    });
}

我的讲师做了完全相同的事情,他直接将每个节点处理程序放入初始化方法中,但我不确定这是否是良好的编码实践,因为从我的角度来看,这会使代码更难阅读。将不同的处理程序放入单独的方法并使用 SceneBuilder 将它们连接到正确的节点会更好,还是将所有内容都放入初始化中被认为是 JavaFX 开发人员的常见编码实践?

这是一个自以为是的,甚至可能是武断的决定,两种方法都可以

在初始化函数中编写事件处理程序与从 FXML 引用事件方法处理程序没有任何问题。

这类事情通常超出 Whosebug 的范围,但无论如何我都会添加一些指示和意见,因为它可能对您或其他人有所帮助,而不管 Whosebug 政策如何。

引用 Scene Builder 中的动作

就个人而言,我会参考 Scene Builder 中的动作:

  • 在 Scene Builder UI 的代码面板部分为突出显示的节点填写 onAction 或其他事件的值。

这还将在 FXML 中添加引用,因此您将得到类似这样的内容,其中包含 onAction 属性的主题标签值:

<Button fx:id="saveButton" mnemonicParsing="false" onAction="#save" text="Save" />

让 Scene Builder 生成骨架 (View | Show Sample Skeleton)。这将创建一个方法签名来填写,像这样:

@FXML
void save(ActionEvent event) {

}

然后将事件处理代码放在那里。

对于该设置,诸如 Idea 之类的 IDE 将对一致性进行额外的智能检查,并允许在 FXML 和 Java 代码之间进行元素导航,这可能很好,尽管这并不是很重要。


以下是有关 JavaFX 控制器的一些重要设计决策的可选附加信息。

如果它让您感到困惑或与您的应用程序无关(这可能适用于小型研究应用程序),请忽略它。

考虑使用 MVC 和共享模型

关于控制器的更重要的设计决策通常是是否使用共享模型和 MVC、MVP 或 MVVM。

我鼓励你研究那个,研究首字母缩略词,然后看看 Eden coding MVC guide

考虑使用依赖注入

还要考虑是否对控制器使用依赖注入框架,例如Spring 集成(对于更复杂的应用程序)或聪明的 eden injection pattern。您不需要使用这些模式,但它们可以提供帮助。 Spring 尤其复杂,与 JavaFX 的集成目前有点棘手。我都知道,所以如果应用程序需要,我会使用它们,但对于其他人来说,这可能不是一个好的组合。

考虑业务服务层

对于中型到大型应用程序,除了具有单独的模型层之外,还应尝试将业务逻辑与控制器分离,以便控制器仅处理调用业务服务上的函数来操作共享模型和绑定该模型到 UI,而不是直接在控制器中实现业务逻辑。

这使得重用、推理和测试业务逻辑变得更加容易。对于较小的应用程序,不需要额外的抽象,您可以在控制器中完成工作。

这样的处理程序通常会调用与共享模型交互的注入服务。如果更新后的数据还需要持久化,那么注入的服务也可以调用数据库访问对象或者restAPI客户端来持久化数据。

综合起来

所以回到前面的例子,你可以像这样在控制器中实现你的保存功能:

public class UserController {

    @FXML
    TextField userName;

    private UserService userService;
    @Autowired
    public void setUserService(UserService userService) {
        userService = userService;
    }

    @FXML
    void save(ActionEvent event) {
        userService.saveUser(new User(userName.getText())); 
    }

}

其中 userService 可能会引用 Spring WebFlux REST 客户端以将新用户持久化到 cloud-deployed REST 服务,或者可能是 Spring Data DAO 以将新用户存储在共享 RDBMS 数据库。

如前所述,并非所有应用程序都需要这种抽象级别。特别是小型应用程序不需要的注入框架。你可以在给定的应用程序中混合架构风格,适当地使用共享模型和服务,如果你喜欢这样编码的话,可以直接在控制器中编写一些较小的功能。如果您混合使用设计模式,请小心,以免弄得一团糟:-)