1.前言
MVC是指Model-View-Controller。其中:
模型(Model):是指业务逻辑,即数据和数据处理;
视图(View):是指交互逻辑,即其它程序(或者人)如何与本程序交互。在大多数的时候就是人机交互逻辑——输入和输出逻辑;
控制器(Controller):是指控制逻辑,即:控制器对视图输入的数据进行转换(验证、过滤等)后调用模型进行处理,然后将模型的计算结果交给视图输出。
因此,只有控制器依赖模型和视图,而视图不依赖控制器和模型,模型也不依赖控制器和视图。下面这幅斯坦福大学公开课上的MVC通信示意图[1],是比较经典和规范地解释了MVC的分层机制。
图1 MVC通信示意图
2.MVC分层的动机
一个计算机程序,不管是大还是小,不管是复杂还是简单,其数据处理过程基本都是:输入-处理-输出。
对小规模问题,这个过程可以在一个方法中编程解决,比如求解一元二次方程,见以下示例代码。
//求一元二次方程的根
public class example1 {
public static void main(String[] args) {
//数据
double[] values = new double[3];//0:二次项系数,1:一次项系数,2:常数
//输入
System.err.println("请输入方程的系数a,b,c:");
Scanner scanner = new Scanner(System.in);
for (int i = 0; i < values.length; i++) {
scanner.hasNextFloat();
values[i] = scanner.nextFloat();
}
//处理
Complex[] roots = new Complex[2];
double delt = values[1] * values[1] - 4 * values[0] * values[2];
if (delt < 0) {
roots[0] = new Complex(-values[1] / (2 * values[0]), Math.sqrt(-delt) / (2 * values[0]));
roots[1] = new Complex(-values[1] / (2 * values[0]), -Math.sqrt(-delt) / (2 * values[0]));
} else {
roots[0] = new Complex((-values[1] + Math.sqrt(delt)) / (2 * values[0]));
roots[1] = new Complex((-values[1] - Math.sqrt(delt) )/ (2 * values[0]));
}
//输出
System.out.println(“x1=”+roots[0]);
System.out.println(“x2=”+roots[1]);
}
private static class Complex {
private double real;
private double image;
public Complex() {
}
public Complex(double real) {
this.real = real;
}
public Complex(double real, double image) {
this.real = real;
this.image = image;
}
@Override
public String toString() {
String result =null;
if (real == 0.0) {
result = image + "i";
} else if (image == 0.0) {
result = "" + real;
} else {
result = real + String.format("%+f", image) + 'i';
}
return result;
}
}
}
从此例中可看到,即便是个核心语句不超过20行的程序代码,也可以明显地划分出输入、处理、输出三个部分。
而对于大规模问题,肯定就不能把这三个部分写到一个方法里,而是要划分成输入模块、数据和数据处理模块、输出模块。这里所谓的模块,可以是方法,也可以是类,还可以是独立的程序。
把程序划分成模块带来的好处有很多,其中一个最大的好处的就是代码重用。
最低级的代码重用,是源代码的拷贝重用。如,在此例中将解一元二次方程的代码(数据处理代码)写到一个方法里:public Complex[] solve(double a,double b,double c)。那如果在另一个问题中也要求解一元二次方程,那就可以把此方法整个拷贝过去。
高级点的代码重用,就是类的重用。再高级点的是程序级的重用,就是:在不修改原有程序的源代码时,在已能正常运行的程序中增加新的功能。即要符合所谓的“开闭原则”——对功能扩展是开放的,但是对于源代码的修改是封闭的、禁止的。要做到这样的重用,首要的是程序模块之间要低耦合。
要做到这一点,就要将输入输出(即View)与数据处理(即Model)分离,不发生直接关系,互不依赖。通过增加中介者(控制器Controller),这个中介者类似于现实中的中间商、经纪人。
3.MVC的初步
下面以CSDN网文Java Swing开发窗体程序开发(四)MVC结构[2]中输入三角形三边求面积为例,说明MVC的最基本的使用,同时也作为对网友weixin_44230933、 mpp001在评论中提问的回复。
先给出根据原文代码逻辑绘制的类图,见图2。
图2 并非真正的MVC设计
从图中可以看到,SimpleWindow是依赖SimpleListener的,SimpleListener本质上还是View的一部分,这个设计其实只V和M两个部分,没有C(控制器)。
下面给出一个真正符合MVC本意设计。
先给出类图如下:
图3 输入三边长求三角形面积的类图设计
此设计中,增加了一个自定义接口ViewEventListener,用以监听视图发射的请求事件。界面类SimpleView中的按钮点击事件、文本框改变事件等等,都是视图内部的事件,是视图内部要处理的,与控制器无关。总之:视图的归视图,控制器的归控制器,模型的归模型。
这样设计的好处是:可以很方便地更换视图界面(如,可以换成一个绘图界面,绘制一个三角形后,计算其面积),而较少地(或无需)修改控制器。
对应的程序代码如下:
public class SimpleView extends JFrame {
private JTextField textFieldA;
private JTextField textFieldB;
private JTextField textFieldC;
private JTextArea resultArea;
private JButton caculateBtn;
//视图事件
ViewEventListener viewEventListener;
public SimpleView() {
//GUI部分
setLayout(new BorderLayout());
textFieldA = new JTextField(5);
textFieldB = new JTextField(5);
textFieldC = new JTextField(5);
resultArea = new JTextArea();//
caculateBtn = new JButton("计算");
JPanel upPanel = new JPanel();//上面板
upPanel.add(new JLabel("边A"));
upPanel.add(textFieldA);
upPanel.add(new JLabel("边B"));
upPanel.add(textFieldB);
upPanel.add(new JLabel("边C"));
upPanel.add(textFieldC);
upPanel.add(caculateBtn);
add(upPanel, BorderLayout.NORTH);
add(new JScrollPane(resultArea), BorderLayout.CENTER);
setVisible(true);
setDefaultCloseOperation(DISPOSE_ON_CLOSE);
setBounds(100, 100, 460, 260);
//给按钮添加监听器,有四种形式{匿名类,自身类,内部类,外部类}
caculateBtn.addActionListener(new MyAction());//用内部类对象监听
}
public void addViewEventListener(ViewEventListener viewEventListener) {
this.viewEventListener = viewEventListener;
}
//内部类
class MyAction implements ActionListener {
@Override
public void actionPerformed(ActionEvent e) {
//获得三条边值
double[] values = new double[3];
values[0] = Double.parseDouble(textFieldA.getText());
values[1] = Double.parseDouble(textFieldB.getText());
values[2] = Double.parseDouble(textFieldC.getText());
//发射View的事件:输入已完成,请求数据处理
String response = viewEventListener.processRequest(values);
//呈现输出结果
resultArea.append(response + "\n");
}
}
}
//-----------------------------------------------------------------------------------
public interface ViewEventListener {
public String processRequest(double[] values);
}
//-----------------------------------------------------------------------------------
public class SimpleController implements ViewEventListener{
private SimpleView myView;//对视图的引用
private Triangle myModel; //对模型的引用
public SimpleController() {
myModel=new Triangle(); //模型实例化
myView=new SimpleView(); //视图实例化
//让控制器监听并处理视图simpleView发射的事件
myView.addViewEventListener(this);
}
@Override
public String processRequest(double[] values) {
myModel.setA(values[0]);
myModel.setB(values[1]);
myModel.setC(values[2]);
return "面积是:"+myModel.getArea();
}
}
//----------------------------------------------------------------------------------
public class Triangle {
private double a;
private double b;
private double c;
public Triangle() {
}
public Triangle(double a, double b, double c) {
this.a = a;
this.b = b;
this.c = c;
}
/**
* 模型自己的方法
*
* @return
*/
public double getArea() {
//计算前应该判定是否是三角形,这里省略了
//利用海伦公式求面积
double p = (a + b + c) / 2.0;
double area = Math.sqrt(p * (p - a) * (p - b) * (p - c));
return area;
}
//省略属性的getter/setter
}
//---------------------------------------------------------------------------------
public class TestApp {
/**
* @param args the command line arguments
*/
public static void main(String[] args) {
new SimpleController ();
}
}
下面是通过绘制三角形输入边长的视图类GraphicView的代码。要使用这个界面,只要修改上述控制器类SimpleController中的两行代码即可(当然,稍加改造就可以做到不修改控制器而直接更换视图)。
public class GraphicView extends JFrame {
static int count = 0;
ViewEventListener viewEventListener;
JTextArea resultArea;
public GraphicView() {
MyPanle graphicPanel = new MyPanle();//绘图面板
add(graphicPanel, BorderLayout.NORTH);//将绘图面板加到该窗口的上部分
resultArea = new JTextArea();
add(new JScrollPane(resultArea), BorderLayout.CENTER);
setVisible(true);
setDefaultCloseOperation(DISPOSE_ON_CLOSE);
setBounds(100, 100, 460, 300);
}
public void addViewEventListener(ViewEventListener viewEventListener) {
this.viewEventListener = viewEventListener;
}
class MyPanle extends JPanel {
private Point[] points = new Point[3];
private Point transientPoint;
private Point curPoint;
public MyPanle() {
curPoint = null;
MouseAdapter ma = new MyMouseAdapter();
addMouseListener(ma);
addMouseMotionListener(ma);
setPreferredSize(new Dimension(200, 150));
}
class MyMouseAdapter extends MouseAdapter {
@Override
public void mouseClicked(MouseEvent e) {
int i = count++ % 3;
if (i == 0) {
points[0] = null;
points[1] = null;
points[2] = null;
}
points[i] = e.getPoint();
curPoint = points[i];
repaint();
if (points[2] != null) {
double deltx = 0;
double delty = 0;
double[] values = new double[3];
for (int k = 0; k < points.length; k++) {
deltx = points[k].getX() - points[(k + 1) % 3].getX();
delty = points[k].getY() - points[(k + 1) % 3].getY();
values[k] = Math.sqrt(deltx * deltx + delty * delty);
}
resultArea.append( viewEventListener.processRequest(values)+"\n");
}
}
@Override
public void mouseMoved(MouseEvent e) {
if (points[2] == null) {
transientPoint = e.getPoint();
repaint();
} else {
transientPoint = null;
}
}
}
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2d = (Graphics2D) g.create();
if (points[0] != null && points[1] != null) {
g2d.setColor(Color.BLUE);
g2d.draw(new Line2D.Double(points[0], points[1]));
}
if (points[2] != null) {
g2d.setColor(Color.BLUE);
g2d.draw(new Line2D.Double(points[2], points[1]));
g2d.setColor(Color.BLUE);
g2d.draw(new Line2D.Double(points[2], points[0]));
}
if (curPoint != null && transientPoint != null) {
g2d.setColor(Color.RED);
g2d.draw(new Line2D.Double(curPoint, transientPoint));
}
g2d.dispose();
}
}
}
4.MVC的理论基础
通过上面的例子,大致了解了MVC的机制。这种程序结构给编程带来了很大的灵活性。视图View只管交互逻辑,不管数据是怎么处理、由谁处理。模型Model,只负责数据和数据处理逻辑,不管数据怎么输入输出、由谁提供、由谁呈现。控制器Controller负责接收视图的提交的数据,交给模型处理,并将模型的处理结果交由视图呈现。
MVC程序结构,是设计模式中观察者模式的一个很成功的具体应用。
观察者模式是一种对象行为模式,包含四个角色[3]。1)抽象目标(Subject):它把所有观察者对象的引用保存到一个聚集里,每个主题都可以有任何数量的观察者。抽象主题提供一个接口,可以增加和删除观察者对象。2)具体目标(ConcreteSubject):将有关状态存入具体观察者对象;在具体主题内部状态改变时,给所有登记过的观察者发出通知。3)抽象观察者(Observer):为所有的具体观察者定义一个接口,在得到主题通知时更新自己。4)具体观察者(ConcreteObserver):实现抽象观察者角色所要求的更新接口,以便使本身的状态与主题状态协调。其类图如下所示。
图4 观察者模式结构图
将图3的view、controller两个包与图4相比较,可知,图3中的视图SimpleView对应图4中的ConcreteSubject,即具体目标(被观察者),在图3中省略了抽象目标的设计;图3中的ViewEventListener就是观察都模式中的抽象观察者(Observer),而控制器SimpleController就是图4中的具体观察者(ConcreteObserver)。
到此,问题得到初步的解决。然而问题还是有的:
(1)前面提及的如何在不修改SimpleController的情况下更换输入界面?
(2) 同样地,在不修改视图和控制器时,怎么更换模型?例如,输入的三边,是一个立方体的边长,求立方体的体积。
参考文献
[1]参考斯坦福大学iOS公开课学习笔记(1)-iOS的MVC框架 - 简书
[3]设计模式,刘伟 主编,清华大学出版社出版,ISBN978-7-30225120-0