MVC是Model-View-Controller的简称,即模型-视图-控制器。MVC是Xerox
PARC在20世纪80年代为编程语言Smalltalk-80发明的一种软件设计模式,至今已被广泛使用。
本章首先介绍MVC设计模式的概念,然后创建一个基于MVC的Java应用,并且在这个Java应用中引入RMI框架,把模型作为远程对象分布到服务器端,把视图和控制器分布到客户端,从而创建分布式的Java应用。
13.1 MVC设计模式简介
MVC把应用程序分成3个核心模块:模型(Model)、视图(View)和控制器(Controller),它们分别担当不同的任务。如图13-1所示显示了这几个模块各自的功能及它们的相互关系。
图13-1 MVC设计模式
1.视图
视图是用户看到并与之交互的界面。视图向用户展示用户感兴趣的业务数据,并能接收用户的输入数据,但是视图并不进行任何实际的业务处理。视图可以向模型查询业务数据,但不能直接改变模型中的业务数据。视图还能接收模型发出的业务数据更新事件,从而对用户界面进行同步更新。
2.模型
模型是应用程序的主体部分。模型表示业务数据和业务逻辑。一个模型能为多个视图提供业务数据。同一个模型可以被多个视图重用。
3.控制器
控制器接收用户的输入并调用模型和视图去完成用户的请求。当用户在视图上选择按钮或菜单时,控制器接收请求并调用相应的模型组件去处理请求,然后调用相应的视图来显示模型返回的数据。
如图13-2所示,MVC的3个模块也可以看做软件的3个层次,最上层为视图层,中间为控制器层,下层为模型层。总地说来,层与层之间为自上而下的依赖关系,下层组件为上层组件提供服务。视图层与控制器层依赖模型层来处理业务逻辑和提供业务数据。此外,层与层之间还存在两处自下而上的调用,一处是控制器层调用视图层来显示业务数据,另一处是模型层通知客户层同步刷新界面。为了提高每个层的独立性,应该使每个层对外公开接口,封装实现细节。
图13-2 MVC的3个模块也可以看做软件的3个层次
4.MVC处理过程
如图13-3所示,首先用户在视图提供的界面上发出请求,视图把请求转发给控制器,控制器调用相应的模型来处理用户请求,模型进行相应的业务逻辑处理,并返回数据。最后控制器调用相应的视图来显示模型返回的数据。
图13-3 MVC的处理过程
5.MVC的优点
首先,多个视图能共享一个模型。在MVC设计模式中,模型响应用户请求并返回响应数据,视图负责格式化数据并把它们呈现给用户,业务逻辑和数据表示分离,同一个模型可以被不同的视图重用,所以大大提高了模型层的程序代码的可重用性。
其次,模型是自包含的,与控制器和视图保持相对独立,因此可以方便地改变应用程序的业务数据和业务规则。如果把数据库从MySQL移植到Oracle,或者把RDBMS数据源改变成LDAP数据源,只需改变模型即可。一旦正确地实现了模型,不管业务数据来自数据库还是LDAP服务器,视图都会正确地显示它们。由于MVC的3个模块相互独立,改变其中一个不会影响其他两个,所以依据这种设计思想能构造良好的松偶合的组件。
此外,控制器提高了应用程序的灵活性和可配置性。控制器可以用来连接不同的模型和视图去完成用户的需求,控制器为构造应用程序提供了强有力的重组手段。给定一些可重用的模型和视图,控制器可以根据用户的需求选择适当的模型进行业务逻辑处理,然后选择适当的视图将处理结果显示给用户。
6.MVC的适用范围
使用MVC需要精心的设计,由于它的内部原理比较复杂,所以需要花费一些时间去理解它。将MVC运用到应用程序中,会带来额外的工作量,增加应用的复杂性,所以MVC不适合小型应用程序。
但对于开发存在大量用户界面,并且业务逻辑复杂的大型应用程序,MVC将会使软件在健壮性、代码重用和结构方面上一个新的台阶。尽管在最初构建MVC框架时会花费一定的工作量,但从长远角度看,它会大大提高后期软件开发的效率。
13.2 store应用简介
本章介绍的Java应用实现了一个商店的客户管理系统,本书把此应用简称为store应用。store应用包含以下用例(Use
Case):
- 创建新客户
- 删除客户
- 更新客户的信息
- 根据客户ID查询特定客户的详细信息
- 列出所有客户的清单
store应用使用MySQL数据库服务器,它的永久业务数据都存放在STOREDB数据库,其中CUSTOMERS表用来存放客户信息,它的定义如下:
create table CUSTOMERS
( ID bigint not null auto_increment primary
key, NAME varchar(16) not null,
AGE INT, ADDRESS varchar(255)
); |
STOREDB数据库的创建过程可参见本书第12章的12.2节(安装和配置MySQL数据库)。如图13-4所示是store应用的类框图。其中ConnectionPool接口、ConnectionPoolImpl2类、ConnectionProvider类和PropertyReader类都来自于本书第12章。ConnectionPool接口表示连接池,负责为模型提供数据库连接。StoreException类是异常类,如例程13-1所示是它的源程序:
图13-4 store应用的类框图
例程13-1 StoreException.java
package store;
public class StoreException extends Exception{
public StoreException() {
this("StoreException"); }
public StoreException(String msg) {
super(msg); }
} |
当模型层处理业务逻辑时出现错误,就会抛出StoreException,例如:
public
void deleteCustomer(Customer cust) throws StoreException,RemoteException{
try{
if(!idExists(cust.getId())){
throw new StoreException("Customer
"+cust.getId()+" not found");
}
String sql="delete from
CUSTOMERS where ID="+cust.getId();
dbService.modifyTable(sql);
fireModelChangeEvent(cust);
}catch(Exception e){
e.printStackTrace();
throw new StoreException("StoreDbImpl.deleteCustomer\n"+e);
}
} |
Customer类与数据库中的CUSTOMERS表对应,它表示store应用的业务数据。模型层负责把Customer对象保存到数据库中,以及从数据库中加载特定的Customer对象。视图层则负责在图形界面上展示Customer对象的信息,以及接收用户输入的Customer对象的信息。如例程13-2所示是Customer类的源程序。
例程13-2 Customer.java
package
store;
import java.io.*;
public class Customer implements Serializable {
private long id;
private String name="";
private String addr="";
private int age;
public Customer(long id,String name,String
addr,int age) {
this.id=id;
this.name=name;
this.addr=addr;
this.age=age;
}
public Customer(long id){
this.id=id;
}
public Long getId(){
return id;
} public
String getName(){
return name;
}
public void setName(String
name){
this.name=name;
}
…
public String toString(){
return "Customer: "+id+"
"+name+" "+addr+" "+age;
}
} |
在分布式运行环境中,Customer对象会从服务器端传送到客户端,也会从客户端传送到服务器端。Customer类实现了java.io.Serializable接口,从而保证Customer对象可以在网络上传输。
- store应用包括3个核心接口。
- StoreView接口:视图层的接口,负责生成与用户交互的图形界面。
- StoreController接口:控制器层的接口,负责调用模型和视图。
- StoreModel接口:模型层的接口,负责处理业务逻辑,访问数据库。
如例程13-3所示是StoreView接口的源程序。它包括以下3个方法。
- addUserGestureListener(StoreController ctrl)方法:在视图中注册处理各种用户动作(比如用户按下【查询客户】按钮)的控制器,参数ctrl指定控制器。
- showDisplay(Object display)方法:在图形界面上显示数据,参数display指定待显示的数据。
- handleCustomerChange()方法:当模型层修改了数据库中某个客户的信息时,同步刷新视图的图形界面。
例程13-3 StoreView.java
package
store;
import java.rmi.*;
public interface StoreView extends Remote{
/** 注册处理用户动作的监听器,即StoreController控制器 */
public void addUserGestureListener(StoreController
ctrl)
throws StoreException,RemoteException;
/** 在图形界面上显示数据,参数display表示待显示的数据 */
public void showDisplay(Object display)throws
StoreException,RemoteException;
/** 当模型层修改了数据库中某个客户的信息时,同步刷新视图层的图形界面
*/
public void handleCustomerChange(Customer
cust)throws StoreException,RemoteException;
} |
以上StoreView接口的handleCustomerChange()方法由模型调用,在分布式运行环境中,模型位于服务器层,视图位于客户层。为了使模型能回调StoreView对象的handleCustomerChange()方法,特地把StoreView接口设计为远程接口,handleCustomer-
Change()方法是远程方法,声明抛出RemoteException。本章13.7节的图13-14展示了模型对视图的回调过程。
如例程13-4所示是StoreController接口的源程序。用户在视图提供的图形界面上会执行各种操作,比如按下【查询客户】、【添加客户】、【删除客户】和【更新客户】按钮,StoreController接口中声明了一系列handleXXX()方法,它们分别响应用户在图形界面做出的某种动作。
例程13-4 StoreController.java
package
store;
public interface StoreController {
/** 处理根据ID查询客户的动作 */
public void handleGetCustomerGesture(long
id);
/** 处理添加客户的动作 */
public void handleAddCustomerGesture(Customer
c);
/** 处理删除客户的动作 */
public void handleDeleteCustomerGesture(Customer
c);
/** 处理更新客户的动作 */
public void handleUpdateCustomerGesture(Customer
c);
/** 处理列出所有客户清单的动作 */
public void handleGetAllCustomersGesture();
} |
如例程13-5所示是StoreModel接口的源程序。StoreModel接口中声明了操纵数据库的一系列方法,这些方法用于添加、更新、删除和查询数据库中的客户信息。此外,StoreModel接口的addChangeListener(StoreView
sv)方法用于在模型中注册视图,当模型修改了数据库中的客户信息时,就可以回调所有注册过的视图的handleCustomer-
Change(Customer cust)方法,以便同步刷新所有的视图。
例程13-5 StoreModel.java
package
store;
import java.rmi.*;
import java.util.*;
public interface StoreModel extends Remote {
/** 注册视图,以便当模型修改了数据库中的客户信息时,可以回调视图的刷新界面的方法
*/
public void addChangeListener(StoreView
sv) throws StoreException,RemoteException;
/** 向数据库中添加一个新的客户 */
public void addCustomer(Customer cust)
throws StoreException,RemoteException;
/** 从数据库中删除一个客户 */
public void deleteCustomer(Customer cust)
throws StoreException,RemoteException;
/** 更新数据库中的客户 */
public void updateCustomer(Customer cust)
throws StoreException,RemoteException;
/** 根据参数id检索客户 */
public Customer getCustomer(long id) throws
StoreException,RemoteException;
/** 返回数据库中所有的客户清单 */
public Set getAllCustomers()
throws StoreException,RemoteException;
} |
如图13-5所示显示了store应用根据用户指定的ID查询客户详细信息的时序图。
图13-5 根据用户指定的ID查询客户详细信息的时序图
用户在视图的图形界面上输入ID,然后按下【查询客户】按钮,StoreView调用StoreController的handleGetCustomerGesture(id)方法处理用户的请求,StoreController调用StoreModel的getCustomer(id)方法从数据库中获得相应的客户信息。StoreController接着调用StoreView的showDisplay(customer)方法在图形界面上显示客户信息。
13.3 创建视图
视图包括StoreView接口、StoreViewImpl类和StoreGui类。StoreGui类利用Swing组件生成图形用户界面。StoreViewImpl类实现了StoreView接口,StoreViewImpl类依赖StoreGui类生成图形界面,并且委托StoreController来处理StoreGui界面上产生的事件。
如图13-6和图13-7所示是store应用的图形用户界面,图13-6显示单个客户的详细信息,图13-7显示所有客户的清单。
图13-6 显示单个客户详细信息的图形界面
图13-7 显示所有客户清单的图形界面
store应用的图形界面主要包括以下面板。
- 选择面板selPan:位于界面的最顶端,包括两个按钮,【客户详细信息】按钮和【所有客户清单】按钮。【客户详细信息】按钮使界面的中央区域显示custPan面板,【所有客户清单】按钮使界面的中央区域显示allCustPan面板。
- 单个客户面板custPan:输出或者输入单个客户的详细信息,并且包括4个按钮,【查询客户】、【更新客户】、【添加客户】和【删除客户】。
- 所有客户面板allCustPan:用javax.swing.JTable组件来显示所有客户的清单。
- 日志面板logPan:显示操作失败时的错误信息。
StoreGui类负责生成如图13-6和图13-7所示的图形界面。如例程13-6所示是StoreGui类的源程序。
例程13-6 StoreGui.java
package
store;
//此处省略import语句
…
public class StoreGui {
//界面的主要窗体组件
protected JFrame frame;
protected Container contentPane;
protected CardLayout card=new CardLayout();
protected JPanel cardPan=new JPanel();
//包含各种按钮的选择面板上的组件
protected JPanel selPan=new JPanel();
protected JButton custBt=new JButton("客户详细信息");
protected JButton allCustBt=new JButton("所有客户清单");
//显示单个客户的面板上的组件
protected JPanel custPan=new JPanel();
protected JLabel nameLb=new JLabel("客户姓名");
protected JLabel idLb=new JLabel("ID");
protected JLabel addrLb=new JLabel("地址");
protected JLabel ageLb=new JLabel("年龄");
protected JTextField nameTf=new
JTextField(25);
protected JTextField idTf=new JTextField(25);
protected JTextField addrTf=new JTextField(25);
protected JTextField ageTf=new JTextField(25);
protected JButton getBt=new JButton("查询客户");
protected JButton updBt=new JButton("更新客户");
protected JButton addBt=new JButton("添加客户");
protected JButton delBt=new JButton("删除客户");
//列举所有客户的面板上的组件
protected JPanel allCustPan=new JPanel();
protected JLabel allCustLb=new JLabel("所有客户清单",SwingConstants.CENTER);
protected JTextArea allCustTa=new JTextArea();
protected JScrollPane allCustSp=new JScrollPane(allCustTa);
String[] tableHeaders={"ID","姓名","地址","年龄"};
JTable table;
JScrollPane tablePane;
DefaultTableModel tableModel;
//日志面板上的组件
protected JPanel logPan=new JPanel();
protected JLabel logLb=new JLabel("操作日志",SwingConstants.CENTER);
protected JTextArea logTa=new
JTextArea(9,50);
protected JScrollPane logSp=new JScrollPane(logTa);
/** 显示单个客户面板 custPan */
public void refreshCustPane(Customer cust){
showCard("customer");
if(cust==null
|| cust.getId()==-1){
idTf.setText(null);
nameTf.setText(null);
addrTf.setText(null);
ageTf.setText(null);
return;
}
idTf.setText(new Long(cust.getId()).toString());
nameTf.setText(cust.getName().trim());
addrTf.setText(cust.getAddr().trim());
ageTf.setText(new Integer(cust.getAge()).toString());
}
/** 显示所有客户面板 allCustPan */
public void refreshAllCustPan(Set
custs){
showCard("allcustomers");
String newData[][];
newData=new String[custs.size()][4];
Iterator it=custs.iterator();
int i=0;
while(it.hasNext()){
Customer cust=it.next();
newData[i][0]=new
Long(cust.getId()).toString();
newData[i][1]=cust.getName();
newData[i][2]=cust.getAddr();
newData[i][3]=new
Integer(cust.getAge()).toString();
i++;
}
tableModel.setDataVector(newData,tableHeaders);
}
/** 在日志面板logPan中添加日志信息 */
public void updateLog(String msg){
logTa.append(msg+"\n");
}
/** 获得客户面板custPan上用户输入的ID
*/
public long getCustIdOnCustPan(){
try{
return Long.parseLong(idTf.getText().trim());
}catch(Exception e){
updateLog(e.getMessage());
return -1;
}
}
/** 获得单个客户面板custPan上用户输入的客户信息 */
public Customer getCustomerOnCustPan(){
try{
return new Customer(Long.parseLong(idTf.getText().trim()),
nameTf.getText().trim(),addrTf.getText().trim(),
Integer.parseInt(ageTf.getText().trim()));
}catch(Exception e){
updateLog(e.getMessage());
return null;
}
}
/** 显示单个客户面板custPan或者所有客户面板allCustPan */
private void showCard(String cardStr){
card.show(cardPan,cardStr);
}
/** 构造方法 */
public StoreGui(){
buildDisplay();
}
/** 创建图形界面 */
private void buildDisplay(){
frame=new JFrame("商店的客户管理系统");
buildSelectionPanel();
buildCustPanel();
buildAllCustPanel();
buildLogPanel();
/** carPan采用CardLayout布局管理器,包括custPan和allCustPan两张卡片
*/
cardPan.setLayout(card);
cardPan.add(custPan,"customer");
cardPan.add(allCustPan,"allcustomers");
//向主窗体中加入各种面板
contentPane=frame.getContentPane();
contentPane.setLayout(new BorderLayout());
contentPane.add(cardPan,BorderLayout.CENTER);
contentPane.add(selPan,BorderLayout.NORTH);
contentPane.add(logPan,BorderLayout.SOUTH);
frame.pack();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setVisible(true);
}
/** 创建选择面板selPan */
private void buildSelectionPanel(){…}
/** 为选择面板selPan中的两个按钮注册监听器
*/
public void addSelectionPanelListeners(ActionListener
a[]){
int len=a.length;
if(len!=2){ return;}
custBt.addActionListener(a[0]);
allCustBt.addActionListener(a[1]);
}
/** 创建单个客户custPan面板 */
private void buildCustPanel(){…}
/** 为单个客户面板custPan中的4个按钮注册监听器 */
public void addCustPanelListeners(ActionListener
a[]){
int len=a.length;
if(len!=4){ return;}
getBt.addActionListener(a[0]);
addBt.addActionListener(a[1]);
delBt.addActionListener(a[2]);
updBt.addActionListener(a[3]);
}
/** 创建所有客户allCustPan面板 */
private void buildAllCustPanel(){
allCustPan.setLayout(new BorderLayout());
allCustPan.add(allCustLb,BorderLayout.NORTH);
allCustTa.setText("all
customer display");
tableModel=new
DefaultTableModel(tableHeaders,10);
table=new JTable(tableModel);
tablePane=new JScrollPane(table);
allCustPan.add(tablePane,BorderLayout.CENTER);
Dimension
dim=new Dimension(500,150);
table.setPreferredScrollableViewportSize(dim);
}
/** 创建日志面板*/
private void buildLogPanel(){…}
} |
StoreGui类中的public类型的方法可分为3类。
(1)让图形界面展示数据的方法
- refreshCustPane(Customer cust):在单个客户面板 custPan上显示参数cust指定的特定客户的信息。
- refreshAllCustPan(Set custs):在所有客户面板
allCustPan上显示参数custs指定的所有客户的信息。
- public void updateLog(String msg):在日志面板上显示参数msg指定的日志信息。
(2)从图形界面上读取数据的方法
- getCustIdOnCustPan():读取单个客户面板custPan上用户输入的ID。
- getCustomerOnCustPan():读取单个客户面板custPan上用户输入的客户信息。
(3)为图形界面上的按钮注册监听器的方法
- addSelectionPanelListeners(ActionListener a[]):为选择面板selPan中的两个按钮注册监听器。
- addCustPanelListeners(ActionListener a[]):为单个客户面板custPan中的4个按钮注册监听器。
StoreViewImpl类实现了StoreView接口。一个StoreViewImpl对象与一个StoreModel对象、一个StoreGui对象,以及若干StoreController对象关联。如例程13-7所示是StoreViewImpl类的源程序。
例程13-7 StoreViewImpl.java
package
store;
//此处省略import语句
…
public class StoreViewImpl extends UnicastRemoteObject
implements StoreView,Serializable{
private transient StoreGui gui;
private StoreModel storemodel;
private Object display;
private ArrayList
storeControllers=
new ArrayList(10);
public StoreViewImpl(StoreModel
model)throws RemoteException {
try{
storemodel=model;
model.addChangeListener(this);
//向model注册自身
}catch(Exception e){
System.out.println("StoreViewImpl
constructor "+e);
}
gui=new StoreGui();
//向图形界面注册监听器
gui.addSelectionPanelListeners(selectionPanelListeners);
gui.addCustPanelListeners(custPanelListeners);
}
/** 注册控制器*/
public void addUserGestureListener(StoreController
b)
throws StoreException,RemoteException{
storeControllers.add(b);
}
/** 在图形界面上展示参数display指定的数据 */
public void showDisplay(Object display)
throws StoreException,RemoteException{
if(!(display instanceof Exception))this.display=display;
if(display
instanceof Customer){
gui.refreshCustPane((Customer)display);
}
if(display instanceof Set){
gui.refreshAllCustPan((Set)display);
}
if(display instanceof Exception){
gui.updateLog(((Exception)display).getMessage());
}
}
/** 刷新界面上的客户信息*/
public void handleCustomerChange(Customer
cust)throws StoreException,RemoteException{
long cIdOnPan=-1;
try{
if(display instanceof
Set){
gui.refreshAllCustPan(storemodel.getAllCustomers());
return;
}
if(display instanceof
Customer){
cIdOnPan=gui.getCustIdOnCustPan();
if(cIdOnPan!=cust.getId())return;
gui.refreshCustPane(cust);
}
}catch(Exception e){
System.out.println("StoreViewImpl
processCustomer "+e);
}
}
/** 监听图形界面上【查询客户】按钮的ActionEvent的监听器 */
transient ActionListener custGetHandler=new
ActionListener(){
public void actionPerformed(ActionEvent
e){
StoreController
sc;
long
custId;
custId=gui.getCustIdOnCustPan();
for(int i=0;i
sc=storeControllers.get(i);
sc.handleGetCustomerGesture(custId);
}
}
};
/** 监听图形界面上【添加客户】按钮的ActionEvent的监听器 */
transient ActionListener custAddHandler=new
ActionListener(){…};
/** 监听图形界面上【删除客户】按钮的ActionEvent的监听器
*/
transient ActionListener custDeleteHandler=new
ActionListener(){…};
/** 监听图形界面上【更新客户】按钮的ActionEvent的监听器
*/
transient ActionListener custUpdateHandler=new
ActionListener(){…};
/** 监听图形界面上【客户详细信息】按钮的ActionEvent的监听器
*/
transient ActionListener custDetailsPageHandler=new
ActionListener(){
public void actionPerformed(ActionEvent
e){
StoreController
sc;
long
custId;
custId=gui.getCustIdOnCustPan();
if(custId==-1){
try{
showDisplay(new Customer(-1));
}catch(Exception ex){ex.printStackTrace();}
}else{
for(int i=0;i
sc=storeControllers.get(i);
sc.handleGetCustomerGesture(custId);
}
}
}
};
/** 监听图形界面上【所有客户清单】按钮的ActionEvent的监听器
*/
transient ActionListener allCustsPageHandler=new
ActionListener(){…};
/** 负责监听单个客户面板custPan上的所有按钮的ActionEvent事件的监听器
*/
transient ActionListener custPanelListeners[]
={custGetHandler,custAddHandler,
custDeleteHandler,custUpdateHandler};
/** 负责监听选择面板selPan上的所有按钮的ActionEvent事件的监听器
*/
transient ActionListener selectionPanelListeners[]={
custDetailsPageHandler,allCustsPageHandler};
} |
在StoreViewImpl类中定义了6个ActionListener监听器,它们分别监听图形界面上的6个按钮发出的ActionEvent事件。例如,以下custGetHandler是【查询客户】按钮发出的ActionEvent事件的监听器:
transient
ActionListener custGetHandler=new ActionListener(){
public void actionPerformed(ActionEvent
e){
StoreController sc;
long custId;
custId=gui.getCustIdOnCustPan();
for(int i=0;i
sc=storeControllers.get(i);
sc.handleGetCustomerGesture(custId);
}
}
}; |
在以上actionPerformed()方法中,先从界面中读取用户输入的ID,然后调用StoreController的handleGetCustomerGesture()方法进行处理。由此可见,视图本身并不处理具体业务逻辑,仅负责输入和输出数据,用户的请求则由控制器来处理。从本章13.4节(创建控制器)的控制器实现中可以看出,控制器实际上也不处理业务逻辑,而是调用模型来处理。
|