没有使用Spring等框架时,如何将Swing GUI与业务逻辑分离?

7

请注意,这是一篇长篇文章,抱歉,但我想要表达清楚我的观点:

我一直在思考如何将Swing GUI与Presentation和Business Logic分离。

在工作中,我必须为某些数据实现3 MD Excel导出,并创建一个小型的Swing对话框以配置导出。

我们没有像Spring这样的框架,因此我不得不自己实现它。

我希望完全将GUI与Business Logic分开,分别完成以下任务:

  • 告诉BL从GUI开始工作
  • 从BL向GUI报告进度
  • 从BL向GUI报告日志记录
  • 将BL结果委托给GUI

当然,GUI不应该注意到BL的实现,反之亦然。

我为上述所有任务创建了几个接口,例如ProgressListenerLogMessageListenerJobDoneListener等, 由Business Logic触发。例如,如果Business Logic想要记录,它会调用:

fireLogListeners("Job has been started");

实现公共接口LogListener的类将被附加到BL,并且现在将收到“作业已启动”日志消息的通知。所有这些监听器目前都由GUI本身实现,通常如下:

public class ExportDialog extends JDialog implements ProgressListener, LogListener, JobFinishedListener,  ErrorListener {

    @Override
    public void jobFinished(Object result){
        // Create Save File dialog and save exported Data to file.
    }

    @Override
    public void reportProgress(int steps){
        progressBar.setValue(progressBar.getValue()+steps);
    }

    @Override
    public void errorOccured(Exception ex, String additionalMessage){
        ExceptionDialog dialog = new ExceptionDialog(additionalMessage, ex);
        dialog.open();
    }

    // etc.
}

“GUI和BL创建类”简单地将GUI(作为所有这些监听器接口)附加到BL上,其代码如下:
exportJob.addProgressListener(uiDialog);
exportJob.addLogListener(uiDialog);
exportJob.addJobFinishedListener(uiDialog);
exportJob.start();

由于所有这些新创建的监听器接口,我现在对此非常不确定,因为它看起来很奇怪。 你认为呢? 你如何将你的Swing GUI组件与BL分离?

编辑: 出于更好的演示目的,我在eclipse中创建了一个演示工作区,文件上传地址为file-upload.net/download-9065013/exampleWorkspace.zip.html 我也将它粘贴到pastebin上,但最好在eclipse中导入那些类,代码很多 http://pastebin.com/LR51UmMp


2
你的方向对我来说似乎是合理的。你提供事件通知的观察者模式的想法是合理的,我也不反对以这种方式分离监听器,因为它为你提供了灵活性,可以更改谁接收什么通知而不重叠职责。编写接口是一个好方法。唯一可能需要的是通过某种动态加载过程加载BL逻辑的工厂模式,但除此之外,我认为你正在朝着正确的方向前进。 - MadProgrammer
1
我会看一下MVP模式。这是分层GUI应用程序最常见的方式。你的方法已经完成了一半,但我认为创建额外的层将增加更多的灵活性。也就是说,你需要将View与Presenter分开。 - Boris the Spider
1
我喜欢你的设计!你的导出工作很好地解耦了,里面没有Swing组件。 - myborobudur
2
你的设计很好,但是你必须小心线程问题:业务逻辑不应该在Swing事件分派线程中运行。但是Swing组件应该始终从事件分派线程中使用。因此,当监听器从业务逻辑接收到事件时,它们应该使用SwingUtilities.invokeLater()来更新UI。 - JB Nizet
我现在正在创建一个示例。完成后会在这里发布。 - Stefano L
显示剩余11条评论
2个回答

2

有几点需要注意。

我建议将uiDialog代码从ExportFunction类中剥离出来,将整个perform方法直接放在主类中。ExportFunction的职责是“导出”,而不是“显示图形用户界面”。

public static void main(String[] args) {
    ExportFunction exporter = new ExportFunction();

    final ExportUIDialog uiDialog = new ExportUIDialog();
    uiDialog.addActionPerformedListener(exporter);

    uiDialog.pack();
    uiDialog.setVisible(true);
}

(Swing.invokeLater()不需要)

您似乎过度设计了。我不知道为什么您希望同时运行许多线程。当您按下按钮时,您只期望有一个线程在运行,对吗?那么就没有必要拥有一个actionPerformedListener数组。

可以使用以下方式代替:

button.addActionListener(new ActionListener() {

    @Override
    public void actionPerformed(ActionEvent arg0) {
        if (startConditionsFulfilled()) {
            fireActionListener(ActionPerformedListener.STARTJOB);
        }
    }

});

为什么不直接这样做:
final ExportJob exportJob = new ExportJob();
exportJob.addJobFinishedListener(this);
exportJob.addLogListener(this);

button.addActionListener(new ActionListener() {

    @Override
    public void actionPerformed(ActionEvent e) {
        exportJob.start();
    }
});

这样一来,您就可以摆脱不真正起作用的ExportFunction了。

您似乎有很多侦听器数组。除非您确实需要它们,否则我不会费心使用它们,而是尽可能保持简单。

而不是:

Thread.sleep(1000);
fireLogListener("Excel Sheet 2 created");
Thread.sleep(1000);

只需:

Thread.sleep(1000);
log("Excelt Sheet 1 created");
Thread.sleep(1000);

log是什么:

private void log(final String message) {
    ((DefaultListModel<String>) list.getModel()).addElement(message);
}

这样做可以使代码更简洁、更清晰。
GUI 不应该知道 BL,但是 BL 必须告诉 GUI 要做什么。你可以通过很多接口进行无限抽象,但在 99.99% 的应用程序中并不需要这样做,尤其是你的应用程序似乎相当简单。
因此,虽然你编写的代码很不错,但我建议你简化和减少接口。这并不需要那么多的工程技术。

嗨,我现在认为应用程序已经很好地耦合了。我无法替换Business Logic实现“ExportJob”,例如使用子类,因为它是硬编码的ExportJob。这就是MadProgrammer所说的“FactoryPattern”,以更灵活地改变业务逻辑实现。此外,如果我正确理解了ExportJob类中log()方法的方法,您直接将BL绑定到UIDialog类。 - Stefano L
如果您让控制器处理事件监听,就可以减少ExportJob的绑定。这样,您只需要从控制器到UI的引用即可。我想我要表达的观点是,除非您正在尝试开发一个框架(如Eclipse OSGi),否则没有必要像您现在这样使用接口来抽象事物。这只是我的个人意见。对我来说,这只是一个更新一些GUI组件的线程,可以用一半的代码编写。 - Oliver Watkins

2

基本上,您的架构对我来说看起来还不错。我想您可能会想知道这是因为您设置了众多的监听器。

解决方案可以是:

a)拥有一个通用的事件类,并使用子类来表示特定的事件。您可以使用访问者模式来实现实际的监听器。

b)使用事件总线(例如:guava)。在事件总线架构中,您的模型将发布事件到事件总线,您的UI对象将从事件总线中监听事件并进行过滤。

有些系统甚至可以使用注释来声明监听器方法。


嗨@khaemuaset,使用事件总线的想法听起来很有趣,我会去看看。 - Stefano L

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接