UML软件工程组织

MVC体系结构及其在Java中的应用

Model - View - Controller简称MVC,也就是模型-视图-控制器体系结构是为那些需要为同样的数据提供多个视图的应用程序而设计的。MVC把程序分为三种对象类型:

1. 模型(Model):维护数据并提供数据访问方法。

2. 视图(View):绘制模型的部分数据或者所有数据的可视图。

3. 控制器(Controller):处理事件。

实际上MVC体系结构是使用面向对象的设计原理使应用程序模块化,分为数据组件,表现组件和输入处理组件分别对应模型,视图和控制器。MVC最早出现在Smalltalk - 80中,并作为一种分离用户界面与基本应用程序数据的方法。简单示意图如下:

谈到MVC体系结构,就不得不说说设计模式。MVC经常作为一个设计模式在各种情况中讨论,但是实际上MVC是一种架构模式,而不是设计模式。怎么说呢?之所以不说MVC是一种设计模式是因为它是由一些较小的设计模式组成的,它是一种更大尺度上的模式,因此没有包含在23种设计模式之中。MVC模式如果从细了考虑的话会涉及到三种设计模式:合成模式,策略模式和观察者模式。而这三种模式又会涉及到另外七种模式,这里就不细说了。

不过,需要提一下的是观察者(Observer)模式。观察者设计模式使得一个对象能够与其所依赖的对象松散地耦合,这些松散耦合的对象通过调用已知接口中声明的方法进行交互,而不是调用特定类中定义的方法,可以避免依赖其他对象的具体类型。举个简单的例子,在Java的事件处理机制中,假设有一个JButton,如果用它来处理一个Action - Event事件,那么就需要为它添加一个监听器,也就是一个实现了监听器接口的对象,这个对象一直在“观察”着这个按钮,一旦按钮被点击,那么它就会通知监听器,并且调用相应的方法(对于ActionListener接口来说,就是actionPerformed方法),同时如果添加了多个监听器,那么事件发生时这些监听器都会被通知到,尽管它们有可能是不同类型的对象。

令人高兴的是,在Java中已经提供了观察者设计模式的实现。那就是java.util包中的抽象类Observable和接口Observer。在下文中会举简单的例子介绍如何使用。

在Java中的Swing组件实现了MVC的一个变形,也就是把视图和控制器合并在一起,这样就是一种代理-模型体系结构,如下图:

再举JButton的例子,每一个JButton组件都有一个相关的ButtonModel,JButton就是ButtonModel的代理。ButtonModel对象维护状态信息,比如是否按下了这个JButton组件,是否激活,以及维护一个ActionListener的列表。而JButton则提供图形表现,比如我们看到的一个带有标签和边框的矩形,同时它也会修改ButtonModel的状态(比如按下按钮的时候)。

下面先举一个利用抽象类Observable和接口Observer实现的简单的利用MVC模式的例子。假设有一个银行,现在需要的是在用户存款和取款的时候将款项的变化结果显示出来。先看看在Together Edtion for JBuilder里的类图:

在这里类Account对应着模型,它保存并维护着数据,也就是用户的余额。类AccountTextView对应着视图,反映余额的现况,它继承自抽象类AbstractAccountView,在这个抽象类中有对所有视图抽象出来的操作,因为一个模型可以对应多个视图来显示,这里只举一个视图的例子,如果想要更多的视图只需要再继承类AbstractAccountView并实现相应的方法即可。类AccountController对应着控制器,负责输入,并且修改模型。

当在AccountController中输入想要取出或者存入的数额时,它就会修改一个Account对象account中的数据:

存钱:

account.deposit(Double.parseDouble(amountTextField.getText()));

取钱:

account.withdraw(Double.parseDouble(amountTextField.getText()));

而在Account中的方法deposit和withdraw调用中都会通知视图AccountTextView来修改应该显示的内容:

setChanged();
notifyObservers();

这两个方法是抽象类Observable提供的,setChanged()表示发生了数据变化,notifyObservers()则会通知所有的Observer对象做出相应的改变。而在AbstractAccountView构造函数中的account . addObserver(this);这句话则是将自己在Account处注册,这样notifyObservers()就能够在数据发生变化的时候通知它了,形式上看起来很像事件监听加一个监听器一样。下面就是源代码了,这里的视图只是一个JPanel,如果要运行显示请将它放在一个JFrame或者其他顶层容器中。

import java.util.*;
import java.awt.*;
import javax.swing.JPanel;

public abstract class AbstractAccountView extends JPanel implements Observer{

private Account account;

public AbstractAccountView(Account a) throws NullPointerException{

if(a == null)

throw new NullPointerException();

account = a;
account.addObserver(this);

}

public Account getAccount(){

return account;

}

protected abstract void updateDisplay();

public void update(Observable observable, Object object){

updateDisplay();

}

}

import javax.swing.*;

public class AccountTextView extends AbstractAccountView{

private JTextField balanceTextField = new JTextField(10);

public AccountTextView(Account account){

super(account);
balanceTextField.setEditable(false);
add(new JLabel("款项:"));
add(balanceTextField);

updateDisplay();

}

public void updateDisplay(){

balanceTextField.setText(getAccount().getBalance() + "");

}

}

import java.util.Observable;
public class Account extends Observable{

private double balance;
private String name;

public Account(String name, double d){
name = name;
setBalance(d);
}

private void setBalance(double b){

balance = b;
setChanged();
notifyObservers();

}

public double getBalance(){

return balance;

}

public void withdraw(double amount) throws IllegalArgumentException{


if(amount < 0)

throw new IllegalArgumentException("不能取出负数大小的钱数");

setBalance(getBalance() - amount);

}

public void deposit(double amount) throws IllegalArgumentException{

if(amount < 0)

throw new IllegalArgumentException("不能存入负数大小的钱数");

setBalance(getBalance() + amount);

}

public String getName(){

return name;

}

}

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

public class AccountController extends JPanel{ 

 private Account account;
 private JTextField amountTextField;
 public AccountController(Account controlledAccount){
  
  super();

  account = controlledAccount;
  amountTextField = new JTextField(10);
  JButton depositButton = new JButton("存款");
  depositButton.addActionListener(

  new ActionListener(){

  public void actionPerformed(ActionEvent event){

  try{
   account.deposit(Double.parseDouble(amountTextField.getText()));
  }
  catch(NumberFormatException exception){

   JOptionPane.showMessageDialog(AccountController.this, "请输入有效的钱数","Error", JOptionPane.ERROR_MESSAGE);
  }

  }
 });
  JButton withdrawButton = new JButton("取款");
  withdrawButton.addActionListener(new ActionListener())
   {
    public void actionPerformed(ActionEvent event){

     try{

      account.withdraw(Double.parseDouble(amountTextField.getText()));

     }
     catch(NumberFormatException exception){

      JOptionPane.showMessageDialog(AccountController.this, "请输入有效的钱数","Error", JOptionPane.ERROR_MESSAGE);

     }

   }

 });

setLayout(new FlowLayout());
add(new JLabel("数额: "));
add(amountTextField);
add(depositButton);
add(withdrawButton);

 }

}

利用MVC可以非常灵活的处理很多问题,在Java中,有许多组件都使用了MVC的变形,代理-模型结构。打开javax.swing你会看到一些xxxModel接口,比如ListModel,TreeModel和TableModel接口,还有抽象类AbstractxxxModel,比如AbstractListModel,AbstractTreeModel和AbstractTableModel。

这些接口定义了一些相应的模型中常用的方法,而AbstractxxxModel则提供了一个基本实现,你可以继承它并覆盖一些方法作特殊的用途。

现在就以JList为例,举个例子说明这种数据和视图分离带来的好处。先看看ListModel要求实现哪些方法:

public interface ListModel{

public int getSize();

public Object getElementAt(int i);

public void addListDataListener(ListDataListener l);

public void removeListDataListener(ListDataListener l);

}

getElementAt方法是告诉JList哪一项应当显示什么内容的,利用这一点,我们可以并不实际存储数据,而是在调用这个方法的时候来产生数据。比如现在要在JList中显示所有n个字母组成的序列(这里n在1-26之间),实际的数据量是26的n次方,存这么多内容显然没有必要,让我们来看看如何利用ListModel动态生成数据吧。这里只是说明一下ListModel对象的代码,有了ListModel对象,将它作为参数传给JList的构造函数就可以生成一个JList了。AbstractListModel实现了ListModel接口并且已经实现了addListDataListener和removeListDataListener方法,你所需要做的就是按照需要写好getSize和getElementAt方法。

public class NLetterListModel extends AbstractListModel {

private int length;

public NLetterListModel(int n) { length = n; }

public int getSize() {

return (int)Math.pow('z' - 'a'+ 1, length);

}

public Object getElementAt(int n) {

StringBuffer r = new StringBuffer();;
for (int i = 0; i < length; i++) {

char c = (char)('a' + n % ('z' - 'a'+ 1));
r.insert(0, c);
n = n / ('z' - 'a'+ 1);

}

return r;

}

}

这样,只要为你的JList加上这一句:JList list=new JList(new NLetterModel(n));能够显示所有n个字母组成序列的列表就生成好了,非常方便,不用保存很多数据就能做到。

本文只是抛砖引玉的介绍了MVC体系结构的概念已经在Java中应用的例子。从中可以看到Java语言提供的功能可以很方便的帮助我们在写应用程序的时候利用MVC体系来设计,同时JList,JTree,JTable等组件由于采用了代理-模型的设计,可以加以非常灵活的应用。希望本文能够为大家继续学习MVC提供一些帮助。


版权所有:UML软件工程组织