MVC程序结构的理论与实践(一)

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框架 - 简书

[2]java swing开发窗体程序开发(四)MVC结构

[3]设计模式,刘伟 主编,清华大学出版社出版,ISBN978-7-30225120-0

  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值