Java MVC - 我是否漏掉了什么?

4
抱歉,这篇文章可能有点冗长,但我已经被困扰了一段时间。最近我阅读了很多关于MVC的内容,以及它在Java Swing领域中的应用,但我仍然无法理解为什么它对于任何比简单玩具示例更复杂的应用程序都有用。但让我们从头开始......
我所有的GUI编程都是在C#/.Net 4.0上完成的,虽然不是很广泛,但足够深入了解MVVM——这是MVC的一个新版本。它是一个非常简单的概念:您使用XAML(组件的XML描述)定义GUI,指定例如表格及其模型、文本字段的字符串值之间的绑定。这些绑定对应于对象属性,您需要完全分开定义。这样,您就可以完全解耦视图和其他部分。此外,所有模型内的更改“几乎”自动地反馈到相应的控件中,事件驱动设计也更加核心等等。
现在回到Java,我们需要使用传统的MVC。让我从一个非常简单的例子开始:我想要一个带有两个组合框和一个按钮的面板。在第一个组合框中选择值会驱动第二个组合框中的值,在第二个组合框中选择值会调用外部服务,基于两个组合框中的值,而按钮将使用外部服务重置第一个组合框中的值。如果我要使用“我的”方法来完成这个任务,我将按照以下步骤进行:
public class TestGUI {
    private JComboBox<String> firstCombo;
    private JComboBox<String> secondCombo;
    private JButton button;

    private ExternalReloadService reloadService;
    private ExternalProcessingService processingService;

    public TestGUI(ExternalReloadService reloadService, ExternalProcessingService processingService) {
        initialise();
        this.reloadService = reloadService;
        this.processingService = processingService;
    }

    private void initialise() {
        firstCombo = new JComboBox<>();
        secondCombo = new JComboBox<>();
        button = new JButton("Refresh");

        firstCombo.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                String value = (String) ((JComboBox) e.getSource()).getSelectedItem();
                reloadSecondCombo(value);
            }
        });

        secondCombo.addPropertyChangeListener(new PropertyChangeListener() {
            @Override
            public void propertyChange(PropertyChangeEvent evt) {
                if (evt.getPropertyName().equals("model")) {
                    ComboBoxModel model = (ComboBoxModel) evt.getNewValue();
                    if (model.getSize() == 0) {
                        String value = (String) model.getSelectedItem();
                        processValues((String) firstCombo.getSelectedItem(), value);
                    }
                }
            }
        });

        secondCombo.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                processValues((String) firstCombo.getSelectedItem(), (String) secondCombo.getSelectedItem());
            }
        });

        button.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                resetValues()
            }


        });
    }

    private void processValues(String selectedItem, String value) {
        processingService.process(selectedItem, value);
        //possibly do sth with result and update ui
    }

    private void reloadSecondCombo(String value) {
        secondCombo.setModel(new CustomModel(reloadService.reload(value)));
    }

    private void resetValues() {
        //Call other external service to pull default data, possibly from DB
    }
}

很明显,尽管代码很短,但这不是一个简单的代码片段。如果我们要使用MVC来完成它,我的第一步将是使用某种控制器来完成所有工作,例如:
public class TestGUI {
    private JComboBox<String> firstCombo;
    private JComboBox<String> secondCombo;
    private JButton button;

    private Constroller controller;

    public TestGUI(Controller controller) {
        this.controller = controller;
        initialise();
    }

    private void initialise() {
        firstCombo = new JComboBox<>();
        secondCombo = new JComboBox<>();
        button = new JButton("Refresh");

        firstCombo.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                String value = (String) ((JComboBox) e.getSource()).getSelectedItem();
               Data d = controller.getReloadedData(value);
               //assiign to combobox
            }
        });
问题1: 视图不应该知道控制器的任何信息,而应该对模型的更新做出响应。
为了克服上述问题,我们可以向模型询问。模型将简单地拥有两个列表,每个下拉框一个。因此,我们有一个(完全无用的)模型、一个视图和一个控制器... 问题2: 如何连接它们?至少有两种不同的技术:直接连接与观察者模式。 问题3: 直接连接 - 那不仅是将初始设置中的所有内容重写为三个独立的类吗?在这种方法中,视图注册一个模型,而控制器同时具有视图和模型。它看起来像这样:
public class TestGUI {
    private JComboBox<String> firstCombo;
    private JComboBox<String> secondCombo;
    private JButton button;

    private Model model;

    public TestGUI(Model m) {
        model = m;
    }

   public void updateSecondValues(){
       model.getSecondValues();
       //do sth
   }
}

public class Controller {

    private TestGUI view;
    private Model model;

    public reloadSecondValues(){
        firstValues = ...//reload using external service
        model.setSecondValues(firstValues);
        view.updateSecondValues();
    }

}

public class Model {

    private Set<String> firstValues;
    private Set<String> secondValues;

    public Set<String> getFirstValues() {
        return firstValues;
    }

    public void setFirstValues(Set<String> firstValues) {
        this.firstValues = firstValues;
    }

    public Set<String> getSecondValues() {
        return secondValues;
    }

    public void setSecondValues(Set<String> secondValues) {
        this.secondValues = secondValues;
    }
}

这比必要的复杂得多,我认为,模型和控制器一直在相互调用:视图 ->(执行某些操作)控制器 ->(更新自己)视图。
问题4 观察者模式 - 在我看来,这甚至更糟糕,尽管它允许我们解耦视图和模型。视图将作为模型的监听器进行注册,模型将通知视图有关任何更改的信息。因此,现在我们需要一个像这样的方法:
public void addListener(ViewListener listener);

我们需要一个ViewListener。现在,我们可能有一个带有一些事件参数的方法,但我们不能用一个方法来处理所有情况。例如,视图如何知道我们只是更新第二个组合框而不是重置所有值、禁用某些内容或从表格中删除项?因此,我们需要为每个更新创建一个单独的方法(基本上将我们在GUI上拥有的方法复制并粘贴到监听器中),使监听器变得庞大。 主要问题 由于我在这里提出了一些问题,我想简要总结一下。 主要问题1 将逻辑拆分为几个对象:如果您想象您有多个面板,每个面板都有许多控件,您将需要为其创建视图、模型和视图,这会导致您正常情况下需要三倍数量的类,因为允许在UI类上进行工作。 主要问题2 无论您使用什么连线技术,最终都会在所有对象上添加方法以允许通信,这将是多余的,如果您简单地将所有内容放在UI中。
作为“将所有内容放在UI中”不是解决方案,我正在尝试获得您的帮助和意见。非常感谢您提前的想法。

不确定这是否有帮助,但我也曾经在从MVVM背景学习MVC时遇到了问题,并发现这个答案在理解两种模式之间的差异方面对我有很大帮助。 - Rachel
我不同意那篇帖子 - 控制器不应该生成模型(视图模型)- 它应该完全是视图模型。 - Bober02
1
我建议您查看适用于Swing应用程序的MVP(Model-View-Presenter)模式。 [1](https://dev59.com/-nI95IYBdhLWcg3w5SOh)和[2](https://dev59.com/m07Sa4cB1Zd3GeqP1zxx) - user800014
好的评论 - 尽管其中一些内容更基于C#而不是Java。 - Bober02
第二个链接提供了两个Java示例。JGoodies还有关于这种模式的演示。 - user800014
1个回答

6

我个人采用了观察者模式。我认为你过于强调了一种方法的复杂性。

你的模型应该是“无用”的,因为它只包含数据并向感兴趣的监听器触发事件。这就是全部优点。你可以在一个类中封装任何业务逻辑和要求,并完全独立于任何特定视图进行单元测试。你甚至可以根据需要以不同的视图显示相同的模型。

控制器负责改变模型。视图从模型接收事件,但要根据用户输入进行更改,则通过控制器。这里的优点再次是解耦和可测试性。控制器完全独立于任何GUI组件;它不知道特定视图。

你的视图表示对数据的特定界面,并提供某些操作。构建视图需要模型和控制器是完全合适的。视图将在模型上注册其侦听器。在这些侦听器内部,它将更新自己的表示形式。如果你有一个不错的UI测试框架,你可以模拟这些事件,并断言视图已成功更新,而不使用真实的模型,这可能需要一些外部服务,如数据库或Web服务。当视图中的UI组件接收到自己的事件时,它们可以调用控制器--同样,使用良好的测试框架,你可以断言已模拟的控制器接收到这些事件,而不实际调用任何真正的操作,如网络调用。

至于你的反对意见--类的数量是一个虚假问题。这比解耦要低得多。如果你真的想要优化类的数量,请将所有逻辑放在名为Main的类中。添加通信方法--再次,你正在解耦事物。这是面向对象编程的一个优点。


好的,就我认同解耦而言,你如何处理可能有10个不同面板的情况,每个面板都有自己的模型和控制器?你会为每个视图拥有一个监听器接口吗?并且在每个模型上注册这一类型的监听器的模型? - Bober02
是的,Java没有像C#和XAML提供的强大的数据绑定。你可以尝试连接到PropertyChangeListener或类似的东西。我可能会将侦听器接口定义为模型的内部类,并且View中的实现将是匿名内部类。在8中的Lambda支持将使事情更容易。 - Thorn G
你的意思是Model会公开一个内部类,然后View会实现它作为View实现Model.Listener吗? - Bober02
视图不一定需要实现它。您以前使用过匿名内部类吗?您可以在View的构造函数中执行以下操作: model.registerFooListener(new FooListener() { public void foo(Event e) { //在此处执行与视图相关的操作,例如更新ComboBox } }); - Thorn G
是的,但观察者模式的整个重点在于不要将模型传递给视图。应该由控制器在model.register(view)中进行注册。 - Bober02

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