7、MVC在桌面应用开发中的应用:以Java Swing为例

桌面应用,说白了就是那种你双击图标就能跑的软件。我早年做Java桌面开发时,最头疼的就是界面和业务逻辑搅在一起。改个按钮颜色,结果数据全乱了。后来用了MVC,才算是把代码理清了。今天我们就拿Java Swing来聊聊,MVC在桌面端到底怎么落地。

7.1 桌面应用为什么需要MVC?

你想想看,一个典型的Swing窗口里,有表格、按钮、输入框。如果所有代码都塞在JFrame里,那维护起来就是噩梦。我见过一个项目,一个窗口类写了8000行,改一个字段要翻半天。

MVC的核心思路就一句话:把数据、界面、控制逻辑拆开。在Swing里,这个拆分特别自然:

  • Model:负责数据存储和业务规则。不关心怎么显示。
  • View:负责界面展示。只从Model读数据,不直接改数据。
  • Controller:负责处理用户操作。它调用Model的方法,然后通知View刷新。

嗯,这里要注意:Swing本身其实已经内置了MVC的影子。比如JTable用的TableModel,就是Model和View分离的体现。但很多新手不知道,直接把数据塞进DefaultTableModel里,结果一换数据源就崩了。

核心原则: View只做展示,Model只做数据,Controller只做调度。谁也别越界。

7.2 一个真实的Swing MVC例子

我们做一个简单的学生信息管理窗口。功能就两个:显示学生列表,点击按钮添加学生。我带你一步步拆开。

7.2.1 Model层

Model就是一个普通的Java类,加上事件通知机制。为什么需要通知?因为View要监听Model的变化。

// StudentModel.java
import java.util.*;
import javax.swing.event.*;

public class StudentModel {
    private List<String> students = new ArrayList<>();
    private List<ChangeListener> listeners = new ArrayList<>();

    public void addStudent(String name) {
        students.add(name);
        // 通知所有监听者:数据变了
        for (ChangeListener l : listeners) {
            l.stateChanged(new ChangeEvent(this));
        }
    }

    public List<String> getStudents() {
        return Collections.unmodifiableList(students);
    }

    public void addChangeListener(ChangeListener l) {
        listeners.add(l);
    }
}

我在项目中遇到过一个问题:有人直接在Model里放Swing的TableModel,结果Model层就依赖了Swing框架。这其实破坏了MVC的纯洁性。Model应该纯POJO,不依赖任何UI库。

7.2.2 View层

View只负责画界面。它从Model读数据,但不直接改Model。用户点击按钮时,View把事件转发给Controller。

// StudentView.java
import javax.swing.*;
import java.awt.*;

public class StudentView {
    private JFrame frame;
    private JList<String> studentList;
    private JButton addButton;
    private JTextField nameField;

    public StudentView() {
        frame = new JFrame("学生管理");
        studentList = new JList<>();
        addButton = new JButton("添加");
        nameField = new JTextField(15);

        JPanel panel = new JPanel();
        panel.add(new JLabel("姓名:"));
        panel.add(nameField);
        panel.add(addButton);

        frame.setLayout(new BorderLayout());
        frame.add(new JScrollPane(studentList), BorderLayout.CENTER);
        frame.add(panel, BorderLayout.SOUTH);
        frame.setSize(400, 300);
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    }

    public void setStudentListModel(ListModel<String> model) {
        studentList.setModel(model);
    }

    public JButton getAddButton() { return addButton; }
    public JTextField getNameField() { return nameField; }

    public void show() { frame.setVisible(true); }
}

你看,View里没有一行业务逻辑。它只是把组件拼起来,暴露必要的getter给Controller用。我建议你养成习惯:View里不要出现if...else判断数据逻辑,那应该是Controller的事。

7.2.3 Controller层

Controller是粘合剂。它监听View的事件,调用Model的方法,然后更新View。

// StudentController.java
import javax.swing.*;
import javax.swing.event.*;

public class StudentController {
    private StudentModel model;
    private StudentView view;

    public StudentController(StudentModel model, StudentView view) {
        this.model = model;
        this.view = view;

        // 把Model的数据绑定到View的列表
        DefaultListModel<String> listModel = new DefaultListModel<>();
        for (String s : model.getStudents()) {
            listModel.addElement(s);
        }
        view.setStudentListModel(listModel);

        // 监听Model变化,同步到View
        model.addChangeListener(e -> {
            DefaultListModel<String> lm = (DefaultListModel<String>) view.getStudentListModel();
            lm.clear();
            for (String s : model.getStudents()) {
                lm.addElement(s);
            }
        });

        // 监听按钮点击
        view.getAddButton().addActionListener(e -> {
            String name = view.getNameField().getText().trim();
            if (!name.isEmpty()) {
                model.addStudent(name);
                view.getNameField().setText("");
            }
        });
    }
}

这里有个细节:Controller里用了DefaultListModel,这是Swing的类。但没关系,Controller本来就是和View打交道的,它依赖Swing是合理的。Model保持纯洁就行。

7.2.4 启动类

// Main.java
public class Main {
    public static void main(String[] args) {
        StudentModel model = new StudentModel();
        StudentView view = new StudentView();
        StudentController controller = new StudentController(model, view);
        view.show();
    }
}

启动时,先创建Model,再创建View,最后把Controller搭桥连起来。顺序不要搞反。我曾经见过有人先创建View,再创建Model,结果View初始化时Model还没准备好,空指针了。

7.3 MVC在Swing中的常见坑

避坑指南: 我曾经在项目里把Swing的JTable直接当Model用,结果表格数据一变,业务逻辑也跟着乱套。记住:JTableTableModel是View的一部分,不是业务Model。
  • 不要把业务逻辑写在TableModel。它只负责提供行列数据,加减乘除应该放在Model层。
  • Controller不要直接操作Swing组件内部状态。比如不要写view.getStudentList().setSelectedIndex(...),应该通过View提供的方法来操作。
  • Model的变更通知要放在Swing事件线程里。如果Model在后台线程改了数据,记得用SwingUtilities.invokeLater刷新View。

7.4 用SVG理清MVC在Swing中的协作关系

下面这张图,是我自己画的一个协作流程。你看一眼就能明白三者的关系。

Model 学生数据列表 addStudent() ChangeListener通知 View JList 展示学生 JButton 添加 JTextField 输入 Controller 监听按钮事件 调用model.addStudent() 刷新View列表 数据变更通知 用户点击按钮 调用addStudent() 刷新列表数据

从图里你能看到:用户点击View的按钮,事件传给Controller;Controller调用Model的方法;Model变化后通知View刷新。这个闭环就是MVC在桌面应用里的标准流程。

7.5 我的一些实战建议

个人习惯: 我通常会在Model里放一个PropertyChangeSupport,这样比ChangeListener更灵活,可以传递具体哪个属性变了。Swing的JTable配合AbstractTableModelfireTableDataChanged(),也是类似的思路。
  • View里不要出现业务关键词。比如“学号不能重复”这种校验,应该放在Model或Controller里。View只负责展示和收集输入。
  • Controller不要做得太胖。如果一个Controller里塞了太多事件处理,可以考虑拆成多个Controller,每个负责一个功能模块。
  • 测试起来很方便。因为Model是纯Java类,你可以直接写JUnit测试,不用启动Swing窗口。我当年重构一个老项目时,就是靠给Model加单元测试,才敢动手改代码。

嗯,最后说一句:MVC不是银弹。如果你的桌面应用特别简单,比如就一个对话框,那硬拆MVC反而啰嗦。但一旦界面超过3个组件,或者数据逻辑稍微复杂一点,MVC的价值就体现出来了。你自己写几个小例子试试,慢慢就能找到感觉。


公众号:蓝海资料掘金营,微信deep3321