从经典的MVC模式到Web三层结构

暑假这段时间,在海康这边实习,参与了海康iVMS-8700客户端的开发,对MVC有了新的认识,将之前写的这篇博客更新一下。有错误的地方,欢迎大家批评指正。


经典的MVC



什么是MVC?

先来说说MVC的起源。

MVC起源

  • 1979年,Trygve Reenskaug 这位牛人在Smalltalk-80系统上首次提出了MVC的概念,最初的时候叫做Model-View-Controller-Editor。
  • 1994年,Gof(Gang of Four)在《Design Patterns: Elements of Reusable Object-Oriented Software》一书中对MVC模式做了深入的解析。
  • Trygve Reenskaug最初提出MVC的目的是为了把数据(Model)和视图(View)分离开来,然后用控制器(Controller)作胶水来粘合M和V之间的关系。
    很显然,这样做的目的是为了实现注意点分离这样一个更高层次的设计理念,也就是让专业的对象做专业的事情,View就只负责视图相关的东西,Model就只负责描述数据模型,Controller负责总控,各自协作,别总掺和到一起乱成一锅粥![3]

下面我们看看MVC中的M,V,C表示的含义

M:Model 模型
负责真正的业务逻辑的处理,是MVC的核心, 模型不依赖于视图和控制器

V:View 视图
视图比较简单,视图就是一个软件的用户界面(UI),比如我们天天在使用的QQ界面

C: Controller 控制器
获取并解释视图中用户的输入,并调用模型处理,控制器实现了对View的不同响应,控制器本身并不处理业务逻辑,而是交给模型处理。

下面是他们之间的一个关系图

这里写图片描述
[1]P532
符号说明:[1]P532表示参考文献1第532页,下同

MVC主要由3个模式组合而成:组合模式,策略模式和观察者模式

视图由于是用户界面,使用了各种控件,控件之间采用了组合模式

视图需要使用控制器来处理用户的输入,如果要实现对用户输入的不同处理,可以更换控制器,他们之间实现了策略模式。视图和控制器之间关系的一个比较经典的解释,可以参考文献[2]P4

视图是模型的状态的显示,视图必须保证它的显示能够正确反映模型的状态,模型一旦发生变换,模型将会通知有关的视图。这样,控制器和视图都可以作为模型的观察者,他们之间就实现了观察者模式。

其实,MVC中还经常使用到适配器模式:使用适配器将模型适配成符合现有视图和控制器的需要的模型。

示例

下面我们举个例子,就会更加清楚的理解MVC各个部分之间的关系了
(注:对策略模式和观察者模式不太清楚的同学,需要先复习一下这两种模式)
下面的小程序的功能就是一个节拍控制器

代码可以在这里下载(路径:combined\djview),建议大家将代码下载下来,然后运行一遍

其中有两个视图,一个是控制节拍的视图,一个是显示节拍的视图
这里写图片描述

这个就是控制节拍的视图,你可以输入节拍,下面按钮分别是设置节拍,减小和增加节拍

设置完了之后,就可以在显示界面上显示,此时,显示的是当前节拍:120

这里写图片描述

下面我们来看一下代码,分别看一下视图,控制器和模型的代码,由于代码很长,大家直接跳过代码,看类图部分。


视图部分

package headfirst.combined.djview;

import java.awt.*;
import java.awt.event.*;
import javax.swing.*;

//视图
//BeatObserver,BPMObserver分别是视图实现的观察者接口
public class DJView implements ActionListener,  BeatObserver, BPMObserver 
{
    BeatModelInterface model;//视图作为模型的观察者
    ControllerInterface controller;//视图使用控制器作为处理用户输入的策略
    JFrame viewFrame;
    JPanel viewPanel;
    BeatBar beatBar;
    JLabel bpmOutputLabel;
    JFrame controlFrame;
    JPanel controlPanel;
    JLabel bpmLabel;
    JTextField bpmTextField;
    JButton setBPMButton;
    JButton increaseBPMButton;
    JButton decreaseBPMButton;
    JMenuBar menuBar;
    JMenu menu;
    JMenuItem startMenuItem;
    JMenuItem stopMenuItem;

    //视图需要使用控制器作为策略,同时视图成为模型的观察者
    public DJView(ControllerInterface controller, BeatModelInterface model) 
    {   
        this.controller = controller;
        this.model = model;
        model.registerObserver((BeatObserver)this);
        model.registerObserver((BPMObserver)this);
    }

    //显示节拍视图
    public void createView() 
    {
        // Create all Swing components here
        //JPanel
        bpmOutputLabel = new JLabel("offline", SwingConstants.CENTER);
        beatBar = new BeatBar();
        beatBar.setValue(0);
        JPanel bpmPanel = new JPanel(new GridLayout(2, 1));
        bpmPanel.add(beatBar);
        bpmPanel.add(bpmOutputLabel);

        //JPanel
        viewPanel = new JPanel(new GridLayout(1, 2));
        viewPanel.add(bpmPanel);

        //JFrame
        viewFrame = new JFrame("View");
        viewFrame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        viewFrame.setSize(new Dimension(100, 80));
        viewFrame.getContentPane().add(viewPanel, BorderLayout.CENTER);
        viewFrame.pack();
        viewFrame.setVisible(true);
    }

    //控制节拍视图
    public void createControls() 
    {
        /////////////////////////////JFrame//////////////////////////////////////
        JFrame.setDefaultLookAndFeelDecorated(true);
        controlFrame = new JFrame("Control");
        controlFrame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        controlFrame.setSize(new Dimension(100, 80));
        controlPanel = new JPanel(new GridLayout(1, 2));


        /////////////////////////////Menubar//////////////////////////////////////
        menuBar = new JMenuBar();
        controlFrame.setJMenuBar(menuBar);


        /////////////////////////////Menu//////////////////////////////////////
        menu = new JMenu("DJ Control");
        menuBar.add(menu);

        //startMenuItem
        startMenuItem = new JMenuItem("Start");
        startMenuItem.addActionListener(new ActionListener() 
        {
            public void actionPerformed(ActionEvent event) 
            {
                controller.start();
            }
        });
        menu.add(startMenuItem);

        //stopMenuItem
        stopMenuItem = new JMenuItem("Stop");
        stopMenuItem.addActionListener(new ActionListener() 
        {
            public void actionPerformed(ActionEvent event) 
            {
                controller.stop();
            }
        });
        menu.add(stopMenuItem); 

        //ExitMenuItem
         JMenuItem exit = new JMenuItem("Quit");
         exit.addActionListener(new ActionListener() {
             public void actionPerformed(ActionEvent event) {
                 System.exit(0);
             }
         });
         menu.add(exit);

         ///////////////////////////主体的上面部分//////////////////////////////////////
         JPanel insideControlPanel = new JPanel(new GridLayout(3, 1));

         //上面部分
         JPanel enterPanel = new JPanel(new GridLayout(1, 2));
         bpmLabel = new JLabel("Enter BPM:", SwingConstants.RIGHT);
         bpmLabel.setBorder(BorderFactory.createEmptyBorder(5,5,5,5));
         bpmOutputLabel.setBorder(BorderFactory.createEmptyBorder(5,5,5,5));
         bpmTextField = new JTextField(2);
         enterPanel.add(bpmLabel);
         enterPanel.add(bpmTextField);


         ///////////////////////////主体的中间部分////////////////////////////////////// 
         //setBPMButton
         setBPMButton = new JButton("Set");
         setBPMButton.setSize(new Dimension(10,40));
         setBPMButton.addActionListener(this);


         ///////////////////////////主体的下面部分//////////////////////////////////////
         JPanel buttonPanel = new JPanel(new GridLayout(1, 2));

         //increaseBPMButton
         increaseBPMButton = new JButton(">>");
         increaseBPMButton.addActionListener(this);
         buttonPanel.add(increaseBPMButton);

         //decreaseBPMButton
         decreaseBPMButton = new JButton("<<");
         decreaseBPMButton.addActionListener(this);
         buttonPanel.add(decreaseBPMButton);

         insideControlPanel.add(enterPanel);
         insideControlPanel.add(setBPMButton);
         insideControlPanel.add(buttonPanel);

         //添加顶层Panel
         controlPanel.add(insideControlPanel);
         controlFrame.getRootPane().setDefaultButton(setBPMButton);

         //在JFrame中添加JPanel
         controlFrame.getContentPane().add(controlPanel, BorderLayout.CENTER);
         controlFrame.pack();
         controlFrame.setVisible(true);
    }

    public void enableStopMenuItem() {
        stopMenuItem.setEnabled(true);
    }

    public void disableStopMenuItem() {
        stopMenuItem.setEnabled(false);
    }

    public void enableStartMenuItem() {
        startMenuItem.setEnabled(true);
    }

    public void disableStartMenuItem() {
        startMenuItem.setEnabled(false);
    }

    //按钮事件
    public void actionPerformed(ActionEvent event) 
    {
        if (event.getSource() == setBPMButton)
        {

            int bpm = Integer.parseInt(bpmTextField.getText());
            controller.setBPM(bpm);
        } 
        else 
            if (event.getSource() == increaseBPMButton) 
            {
                //将用户的输入交给控制器,这里为增加节拍
                controller.increaseBPM();
            } 
            else 
                if (event.getSource() == decreaseBPMButton) 
                {
                    controller.decreaseBPM();
                }
    }

    //更新状态,这里作为观察者
    public void updateBPM() 
    {
        if (model != null) 
        {
            int bpm = model.getBPM();
            if (bpm == 0) 
            {
                if (bpmOutputLabel != null) 
                {
                    bpmOutputLabel.setText("offline");
                }
            } 
            else 
            {
                if (bpmOutputLabel != null)
                {
                    bpmOutputLabel.setText("Current BPM: " + model.getBPM());
                }
            }
        }
    }

    //更新状态,这里作为观察者
    public void updateBeat() 
    {
        if (beatBar != null) {
             beatBar.setValue(100);
        }
    }
}



控制器部分

接口

package headfirst.combined.djview;

//控制器接口
//视图能够调用的控制器方法都在这里
public interface ControllerInterface 
{
    void start();
    void stop();
    void increaseBPM();
    void decreaseBPM();
    void setBPM(int bpm);
}

实现

package headfirst.combined.djview;

public class BeatController implements ControllerInterface 
{
    BeatModelInterface model;
    DJView view;

    //这里为什么要通过构造函数传递一个模型呢?而不是直接在控制器中实例化一个模型?
    //因为实际上,模型是可以更换的,而且我们可以通过适配器模式,将一个非模型转换为模型,如后面的心脏适配器
    public BeatController(BeatModelInterface model) 
    {
        this.model = model;
        view = new DJView(this, model);
        view.createView();
        view.createControls();
        view.disableStopMenuItem();
        view.enableStartMenuItem();
        model.initialize();
    }

    public void start() {
        model.on();
        view.disableStartMenuItem();
        view.enableStopMenuItem();
    }

    public void stop() {
        model.off();
        view.disableStopMenuItem();
        view.enableStartMenuItem();
    }

    public void increaseBPM() 
    {
        //控制器会解释用户输入,并调用模型处理
        int bpm = model.getBPM();
        model.setBPM(bpm + 1);
    }

    public void decreaseBPM() 
    {
        int bpm = model.getBPM();
        model.setBPM(bpm - 1);
    }

    public void setBPM(int bpm) 
    {
        //交给模型处理
        model.setBPM(bpm);
    }
}



模型部分

模型接口

package headfirst.combined.djview;

//模型是负责处理真正的业务逻辑
public interface BeatModelInterface 
{
    void initialize();

    void on();

    void off();

    void setBPM(int bpm);

    int getBPM();

    //模型中实现了观察者模式
    void registerObserver(BeatObserver o);

    void removeObserver(BeatObserver o);

    void registerObserver(BPMObserver o);

    void removeObserver(BPMObserver o);
}

模型实现

package headfirst.combined.djview;

import javax.sound.midi.*;
import java.util.*;

//模型不依赖于视图和控制器,所以不需要有构造函数传递参数
public class BeatModel implements BeatModelInterface, MetaEventListener 
{
    Sequencer sequencer;
    ArrayList beatObservers = new ArrayList();
    ArrayList bpmObservers = new ArrayList();
    int bpm = 90;
    Sequence sequence;
    Track track;

    public void initialize() {
        setUpMidi();
        buildTrackAndStart();
    }

    public void on() {
        sequencer.start();
        setBPM(90);
    }

    public void off() {
        setBPM(0);
        sequencer.stop();
    }

    public void setBPM(int bpm) 
    {
        this.bpm = bpm;
        sequencer.setTempoInBPM(getBPM());

        //状态发生改变,通知视图改变状态
        notifyBPMObservers();
    }

    public int getBPM() {
        return bpm;
    }

    void beatEvent() {
        notifyBeatObservers();
    }


    public void registerObserver(BeatObserver o) {
        beatObservers.add(o);
    }

    public void notifyBeatObservers() {
        for(int i = 0; i < beatObservers.size(); i++) {
            BeatObserver observer = (BeatObserver)beatObservers.get(i);
            observer.updateBeat();
        }
    }

    public void registerObserver(BPMObserver o) {
        bpmObservers.add(o);
    }

    public void notifyBPMObservers() 
    {
        for(int i = 0; i < bpmObservers.size(); i++) {
            BPMObserver observer = (BPMObserver)bpmObservers.get(i);
            observer.updateBPM();
        }
    }


    public void removeObserver(BeatObserver o) {
        int i = beatObservers.indexOf(o);
        if (i >= 0) {
            beatObservers.remove(i);
        }
    }



    public void removeObserver(BPMObserver o) {
        int i = bpmObservers.indexOf(o);
        if (i >= 0) {
            bpmObservers.remove(i);
        }
    }


    public void meta(MetaMessage message) {
        if (message.getType() == 47) {
            beatEvent();
            sequencer.start();
            setBPM(getBPM());
        }
    }

    public void setUpMidi() {
        try {
            sequencer = MidiSystem.getSequencer();
            sequencer.open();
            sequencer.addMetaEventListener(this);
            sequence = new Sequence(Sequence.PPQ,4);
            track = sequence.createTrack();
            sequencer.setTempoInBPM(getBPM());
        } catch(Exception e) {
                e.printStackTrace();
        }
    } 

     public void buildTrackAndStart() {
        int[] trackList = {35, 0, 46, 0};

        sequence.deleteTrack(null);
        track = sequence.createTrack();

        makeTracks(trackList);
        track.add(makeEvent(192,9,1,0,4));      
        try {
            sequencer.setSequence(sequence);                    
        } catch(Exception e) {
            e.printStackTrace();
        }
    } 

    public void makeTracks(int[] list) {        

       for (int i = 0; i < list.length; i++) {
          int key = list[i];

          if (key != 0) {
             track.add(makeEvent(144,9,key, 100, i));
             track.add(makeEvent(128,9,key, 100, i+1));
          }
       }
    }

    public  MidiEvent makeEvent(int comd, int chan, int one, int two, int tick) {
        MidiEvent event = null;
        try {
            ShortMessage a = new ShortMessage();
            a.setMessage(comd, chan, one, two);
            event = new MidiEvent(a, tick);

        } catch(Exception e) {
            e.printStackTrace(); 
        }
        return event;
    }
}



三者之间的关系

这里写图片描述

我们先来看MVC中的第一个设计模式,组合模式,这个模式比较简单,看上面的代码,视图层用到了很多控件,包括JButton,JPanel等,这些对象很多会划分一组,并将该组对象当做一个对象使用
如下代码:

///////////////////////////主体的下面部分//////////////////////////////////////
JPanel buttonPanel = new JPanel(new GridLayout(1, 2));

//increaseBPMButton
increaseBPMButton = new JButton(">>");
increaseBPMButton.addActionListener(this);
buttonPanel.add(increaseBPMButton);

//decreaseBPMButton
decreaseBPMButton = new JButton("<<");
decreaseBPMButton.addActionListener(this);
buttonPanel.add(decreaseBPMButton);

insideControlPanel.add(buttonPanel);

这里buttonPanel就被当做了一个对象使用
这个就是非常典型的组合模式了.

看视图层代码,BeatObserver,BPMObserver分别是视图实现的观察者接口
这里我们可以看到,视图使用了控制器作为处理用户输入的策略,同时视图作为模型的观察者,需要实现观察者接口
现在我们来增加节拍,点击增加节拍按钮,首先,视图层对增加节拍按钮响应的事件处理代码

//按钮事件
    public void actionPerformed(ActionEvent event)
 {
  if (event.getSource() == setBPMButton)
  {

   int bpm = Integer.parseInt(bpmTextField.getText());
         controller.setBPM(bpm);
  }
  else
   if (event.getSource() == increaseBPMButton)
   {
    //将用户的输入交给控制器,这里为增加节拍
    controller.increaseBPM();
   }
   else
    if (event.getSource() == decreaseBPMButton)
    {
     controller.decreaseBPM();
    }
    }

我们可以看到,视图将用户的输入交给控制器,那么控制器呢?

public void increaseBPM()
 {
       //控制器会解释用户输入,并调用模型处理
        int bpm = model.getBPM();
        model.setBPM(bpm + 1);
 }

控制器交给了模型去处理了,模型实现了真正的业务逻辑的处理

public void setBPM(int bpm)
    {
  this.bpm = bpm;
  sequencer.setTempoInBPM(getBPM());

  //状态发生改变,通知视图改变状态
  notifyBPMObservers();
    }

模型处理完后,状态发生了改变,就需要通知视图,我状态改变了,你需要向用户展现,我们来看看notifyBPMObservers方法

public void notifyBPMObservers()
 {
  for(int i = 0; i < bpmObservers.size(); i++) {
   BPMObserver observer = (BPMObserver)bpmObservers.get(i);
   observer.updateBPM();
  }
 }

这个方法就通知了所有的观察者,更新状态,前面我们说过了视图实现了BPMObserver,所以视图会使用updateBPM方法更新界面
这里写图片描述

结合刚刚的图,我们再将它们之间的关系想一遍:视图将用户的输入交给控制器,控制器解读用户的输入,然后调用模型,由模型去处理真正的业务逻辑,模型的处理完后,告诉视图更新状态,将新的状态展示给用户。

这里写图片描述

如果现在要更换处理用户数据的方式,那么只需要更改控制器就可以了。这就实现了策略模式。

这就是一个比较典型的MVC模式。好了,现在我们回到刚刚的那个问题:MVC到底是不是一种设计模式?
参考文献[1]中说MVC是一种复合模式,他也没说清楚到底是不是一种设计模式,但是参考文献[2]也就是GOF的23种设计模式里面没有MVC,这是为什么呢?

为什么GOF的23种设计模式里面没有MVC?
文献[3]:为什么MVC不是一种设计模式
中解释的比较好,大家可以好好看看,这里我直接引用过来

GoF (Gang of Four,四人组, 《Design Patterns: Elements of Reusable Object-Oriented Software》/《设计模式》一书的作者:Erich Gamma、Richard Helm、Ralph Johnson、John Vlissides)并没有把MVC提及为一种设计模式,而是把它当做“一组用于构建用户界面的类集合”。在他们看来,它其实是其它三个经典的设计模式的演变:观察者模式(Observer)(Pub/Sub), 策略模式(Strategy)和组合模式(Composite)。根据MVC在框架中的实现不同可能还会用到工厂模式(Factory)和装饰器(Decorator)模式。

正如我们所讨论的,models表示应用的数据,而views处理屏幕上展现给用户的内容。为此,MVC在核心通讯上基于推送/订阅模型(惊讶的是 在很多关于MVC的文章中并没有提及到)。当一个model变化时它对应用其它模块发出更新通知(“publishes”),订阅者 (subscriber)——通常是一个Controller,然后更新对应的view。观察者——这种自然的观察关系促进了多个view关联到同一个 model。
对于感兴趣的开发人员想更多的了解解耦性的MVC(根据不同的实现),这种模式的目标之一就是在一个主题和它的观察者之间建立一对多的关系。当这个 主题改变的时候,它的观察者也会得到更新。Views和controllers的关系稍微有点不同。Controllers帮助views对不同用户的输 入做不同的响应,是一个非常好的策略模式列子。

其实MVC是使用组合,策略和观察者模式而实现的一种软件结构,并不能说是一种设计模式,MVC的目的是解耦,将视图和模型分开,增加系统的扩展性和可维护性。下面我们来看看经典MVC的变体。


Web中的MVC

经典MVC模式在Web上的应用,就产生了现在我们看到的Web三层结构。
说到网站开发中的三层结构,上网一搜,一大推,这里我给出几篇我觉得写的比较好的

http://blog.csdn.net/yangyuankp/article/details/7880943
http://www.cnblogs.com/kelvin0916/archive/2012/09/24/2700264.html

大部分都喜欢将三层结构划分为:

  • 表示层UI
  • 业务逻辑层BLL
  • 数据访问层DAL

这里,我还是将这三层表示为模型-视图-控制器。但是模型层可以划分为N层。

我们先来看一下Web管理系统中比较通用的结构
这里写图片描述

相信开发过Web管理系统的同学对这个结构还是比较熟悉的,这里重点说一下模型。

实体类
实体类就是数据库表的映射,即一个实体类表示数据库中的一张表,每个属性实现get和set方法。
为什么要使用实体类,可以参考这篇博客:http://blog.csdn.net/yangyuankp/article/details/7880943

这里顺便说一下实体类,控制类,和边界类之间的关系。

在系统分析与设计阶段,类通常可以分为三种,分别是实体类(Entity Class)、控制类(Control Class)和边界类(Boundary Class):
(1) 实体类:实体类对应系统需求中的每个实体,它们通常需要保存在永久存储体中,一般使用数据库表或文件来记录,实体类既包括存储和传递数据的类,还包括操作数据的类。实体类来源于需求说明中的名词,如学生、商品等。
(2) 控制类:控制类用于体现应用程序的执行逻辑,提供相应的业务操作,将控制类抽象出来可以降低界面和数据库之间的耦合度。控制类一般是由动宾结构的短语(动词+名词)转化来的名词,如增加商品对应有一个商品增加类,注册对应有一个用户注册类等。
(3) 边界类:边界类用于对外部用户与系统之间的交互对象进行抽象,主要包括界面类,如对话框、窗口、菜单等。

数据库操作类
数据库操作类就是DBUtility/SqlHelper,这里通常是针对接口编程的,已应对不同数据库,通常项目中,使用数据库连接池来管理数据库连接,海康的项目中就采用了这些技术。

数据库访问层DAL
数据库访问层实现了对每张数据库表的增,删,改,查操作。调用数据库操作类提供的接口。

业务逻辑层BLL
业务逻辑层就是实现了与项目有关的具体业务,调用DAL层提供的接口

上面的图,只是一种通用的结构,下面我们分别看看java和.NET中的具体应用。

java web中的MVC

先来看看大家比较熟悉的Java Web
这里写图片描述

[1]P549
java Web的模式大致如上图
整个后面部分为模型
这里写图片描述

这里的对应关系为:

  • JSP:视图
  • servlet:控制器
  • javabean:模型

命名

在java Web项目中,通常将每个部分命名为

  • service:对应业务逻辑层 BLL
  • dao:对应数据访问层 DAL
  • domain :对应实体类 Entity
  • DBUtility: 对应数据库连接类

所以,如果看到java Web中这些命名,要知道对应哪个部分。



ASP.NET中的三层结构

ASP.NET中

  • .aspx: 视图层
  • .aspx.cs: 每个.aspx页面都会有一个.aspx.cs文件,原来我一直不理解这个文件有啥作用,现在发现,他的作用就相当于是控制器,处理视图中用户的输入,然后交给模型处理
  • .cs: 模型

下面总结一下
这里写图片描述

在实际的实现过程中,根据项目的需求,模型可以划分为多个层次,而不是固定的BLL,和DAL

由于图片比较大,网页上可能看不清楚,我上传到CSDN上了,大家可以下载,点击下载

注意:
旁边的代码,是当时复习MVC的时候,为了更好理解而加上的,代码可能不够准确,大家只看主题部分即可。


Web与经典MVC的区别

Web中视图和控制器不是真正意义上的策略模式,视图和模型之间也不是真正意义上的观察者模式,所以Web无法实现真正意义上的MVC。



结束语

今天是端午节,昨天在京东上买的粽子才到,早上跟室友一起吃粽子,也算是过节了,今天没有回去,也没有出去找同学玩,就想一个人在学校看书,因为发现自己有很多东西需要学习,想多用点时间看点书,计算机的世界美妙而又神奇,从算法到软件体系结构,到各种编程语言,再到计算机视觉领域,无不充满了迷人的魅力!今天总结了一下这几天看的MVC模式,写了篇博客,感觉充实而又快乐。下个月就要去杭州的海康威视实习了,未来的路就想计算机世界一样,美妙而又神奇。最后,祝大家端午节快乐!

2015-06-20 22:57
Last updated :2015-8-31 16:48:24



参考文献

[1] 《Head First设计模式(中文版)》 ,中国电力出版社
[2] 《设计模式:可复用面向对象软件的基础》(著名的GOF设计模式),机械工业出版社
[3] 为什么MVC不是一种设计模式

  • 3
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值