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里,结果一换数据源就崩了。
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中的常见坑
JTable直接当Model用,结果表格数据一变,业务逻辑也跟着乱套。记住:JTable的TableModel是View的一部分,不是业务Model。
- 不要把业务逻辑写在
TableModel里。它只负责提供行列数据,加减乘除应该放在Model层。 - Controller不要直接操作Swing组件内部状态。比如不要写
view.getStudentList().setSelectedIndex(...),应该通过View提供的方法来操作。 - Model的变更通知要放在Swing事件线程里。如果Model在后台线程改了数据,记得用
SwingUtilities.invokeLater刷新View。
7.4 用SVG理清MVC在Swing中的协作关系
下面这张图,是我自己画的一个协作流程。你看一眼就能明白三者的关系。
从图里你能看到:用户点击View的按钮,事件传给Controller;Controller调用Model的方法;Model变化后通知View刷新。这个闭环就是MVC在桌面应用里的标准流程。
7.5 我的一些实战建议
PropertyChangeSupport,这样比ChangeListener更灵活,可以传递具体哪个属性变了。Swing的JTable配合AbstractTableModel的fireTableDataChanged(),也是类似的思路。
- View里不要出现业务关键词。比如“学号不能重复”这种校验,应该放在Model或Controller里。View只负责展示和收集输入。
- Controller不要做得太胖。如果一个Controller里塞了太多事件处理,可以考虑拆成多个Controller,每个负责一个功能模块。
- 测试起来很方便。因为Model是纯Java类,你可以直接写JUnit测试,不用启动Swing窗口。我当年重构一个老项目时,就是靠给Model加单元测试,才敢动手改代码。
嗯,最后说一句:MVC不是银弹。如果你的桌面应用特别简单,比如就一个对话框,那硬拆MVC反而啰嗦。但一旦界面超过3个组件,或者数据逻辑稍微复杂一点,MVC的价值就体现出来了。你自己写几个小例子试试,慢慢就能找到感觉。