什么是MVC
MVC三个单词分别是model(模型)、view(视图)、control(控制),MVC设计模式的特点就是把模型、视图、控制三个部分的代码分开写,使得各个部分最大限度地实现松耦合,视图部分持有模型部分和控制部分的引用,控制部分持有视图部分和模型部分的引用,而模型部分不持有它们的引用。如果要实现模型对视图的影响,可以使用观察者模式,在模型中注册视图的观察者对象,当有信息更新的时候可以通知所有观察者。
下图即MVC的基本模型:
下面通过MVC设计模式实现一个简单的节拍器。
视图(V)
1、构造方法。视图的构造方法传入模型和控制器的引用,并且调用模型的registerObserver方法注册称为观察者。这里用到了两个视图,于是实现了两个观察者接口,都有update方法,在“主题”更新的时候可以得到通知。
/**
* 构造方法,获得M和C的引用
* 在模型中注册,称为模型的观察者,以便获取数据更新的通知
* @param model
* @param controller
*/
public DJView(BeatModelInterface model, ControllerInterface controller) {
this.model = model;
this.controller = controller;
model.registerObserver((BeatObserver)this);
model.registerObserver((BPMObserver)this);
}
public interface BPMObserver {
void updateBPM();
}
public interface BeatObserver {
void updateBeat();
}
2、视图持有的引用。在如下例子中我们可以发现,视图部分除了模型和控制器的引用,其他的都是组件的申明,代码十分干净,视图只负责显示。
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;
3、视图的创建代码。这部分代码实例化了组件,调用这个方法就可以使得视图显示出来。如果有按钮,可以直接实现监听器方法,按钮是用来触发控制器的方法的,这个在后面还会提到。
/**
* 创建视图1,显示参数部分
* 调用这个方法实例化所有组件并创建视图
* 这段代码只管显示(不涉及任何逻辑)
*/
public void creatView(){
viewPanel=new JPanel(new GridLayout(1, 2));
viewFrame=new JFrame("View");
viewFrame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
viewFrame.setSize(new Dimension(100, 80));
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);
viewPanel.add(bpmPanel);
viewFrame.getContentPane().add(viewPanel, BorderLayout.CENTER);
viewFrame.pack();
viewFrame.setVisible(true);
}
加监听的例子:
startMenuItem.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
controller.start();
}
});
4、这些方法只涉及到对组件的显示操作,所有的数据都要从model中拿。
/**
* 允许使用停止按钮
*/
public void enableStopMenuItem(){
stopMenuItem.setEnabled(true);
}
/**
* 禁止使用停止按钮
*/
public void disableStopMenuItem(){
stopMenuItem.setEnabled(false);
}
/**
* 允许使用开始按钮
*/
public void enableStartMenuItem(){
startMenuItem.setEnabled(true);
}
/**
* 禁止使用开始按钮
*/
public void disableStartMenuItem(){
startMenuItem.setEnabled(false);
}
/**
* 更新数据的方法,从模型中拿数据
* 更新bpm参数
*/
public void updateBPM() {
int bpm=model.getBPM();
if(bpm==0){
bpmOutputLabel.setText("offline");
}else{
bpmOutputLabel.setText("Current BPM:"+model.getBPM());
}
}
/**
* 更新跳动框的数值
*/
public void updateBeat() {
beatBar.setValue(100);
}
小结:视图部分仅仅做显示这件事情,不直接涉及到对数据的改动,只在需要的时候从模型中获取数据,也与控制器无关,它的这些方法是用来被控制器和模型调用的。
控制器(C)
1、构造方法。获取传入的模型引用,new视图,初始化视图和模型。
/**
* 构造方法,获得模型和视图的引用
* 在控制部分调用创建视图的方法对视图做一些初始化的控制操作
* 在控制部分调用模型的初始化方法
* @param model
*/
public BeatController(BeatModelInterface model) {
this.model = model;
view=new DJView(model, this);
view.creatView();
view.createControls();
view.enableStartMenuItem();
view.disableStopMenuItem();
model.initialize();
}
2、它持有的引用。只有模型和视图的引用,真的很简洁。
BeatModelInterface model;
DJView view;
3、监听器触发的控制器方法,这些方法都是和按钮等事件直接关联的。如下是接口以及实现类中的方法。它的方法中一般包括两个部分,一个部分是对视图的显示的改变,另一个部分是调用模型的方法,让模型改变状态。
public interface ControllerInterface {
void start();
void stop();
void increaseBPM();
void decreaseBPM();
void setBPM(int bpm);
}
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);
}
小结:控制器只管控制,它的方法由视图中的组件来触发。
模型(M)
模型的接口和实现类就比较多元化了,模型处理复杂的数据和逻辑,直接接触被控制的对象,并获取和反馈数据。各种业务逻辑都在模型的代码中实现。
1、构造方法。可以发现,这些方法是对节拍的直接控制,get方法是用来给视图获取信息用的(注意哦,视图只能get数据,绝对不能set数据)。下面四个方法是注册和移除观察者的方法(观察者模式)。
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);
}
2、抽象工厂模式。在接口中有一个initialize()方法,这是一个抽象工厂方法,子类需要实现它,在这个方法里,完成所有申明的对象的实例化。
(部分代码)
Sequencer sequencer;//定序器
ArrayList beatObservers=new ArrayList();//数据显示框的观察者列表
ArrayList bpmObservers=new ArrayList();//数据设置框的观察者列表
int bpm=90;//节拍值
Sequence sequence;//Sequence 是一种数据结构,包含可由 Sequencer 对象回放的音乐信息(通常是整首歌曲或音乐作品)。
Track track;//Track 占用 Sequencer 演奏的数据层次中的一个中间层
/**
* 初始化操作,刚开始执行一次
*/
public void initialize() {
setUPMidi();
buildTrackAndStart();
}
/**
* 设置音乐
*/
private 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();
}
}
3、设置节拍数据的方法:
/**
* 启动音乐
*/
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();//节拍变化,通知BPM观察者
}
/**
* 获取节拍值
*/
public int getBPM() {
return bpm;
}
/**
* 发生了打击事件
*/
void beatEvent(){
notifyBeatObservers();//通知beat观察者
}
4、观察者模式涉及的代码。ArrayList存放观察者,在通知的时候,分别取出来调用update方法就可以了。
/**
* 注册beat观察者
*/
public void registerObserver(BeatObserver o) {
beatObservers.add(o);
}
/**
* 注册BPM观察者
*/
public void registerObserver(BPMObserver o){
bpmObservers.add(o);
}
/**
* 移除beat观察者
*/
public void removeObserver(BeatObserver o){
int i=beatObservers.indexOf(o);
if(i>=0){
beatObservers.remove(i);
}
}
/**
* 移除BPM观察者
*/
public void removeObserver(BPMObserver o){
int i=bpmObservers.indexOf(o);
if(i>=0){
bpmObservers.remove(i);
}
}
/**
* 通知beat观察者
*/
public void notifyBeatObservers(){
for(int i=0;i<beatObservers.size();i++){
BeatObserver observer=(BeatObserver)beatObservers.get(i);
observer.updateBeat();//更新
}
}
/**
* 通知BPM观察者
*/
public void notifyBPMObservers(){
for(int i=0;i<bpmObservers.size();i++){
BPMObserver observer=(BPMObserver)bpmObservers.get(i);
observer.updateBPM();//更新
}
}
小结:这部分代码灵活性很大,实现各种逻辑和数据处理,get方法、注册观察者方法、移除观察者方法由view来调用,其他方法由控制器调用,从而实现状态的修改。
策略模式和适配器模式在MVC中的应用
除了之前提到的观察者模式和工厂模式之外,我们还可以在MVC中用到策略模式和适配器模式。
策略模式:策略模式的一般思路是使用组合,把变化的行为抽象出来变成接口,以委托的方式实现行为的动态变化。一般策略模式可以应用在模型的这个环节,使得行为的调用和行为的具体实现解耦。
适配器模式:假如我们要把上述的节拍器换成一个心跳的检测器,在不重写代码的条件下,为了适应原有的代码结构,我们一般会使用适配器模式来解决问题。模型部分,我们要把心跳模型适配成节拍模型,这样就可以适应原先的代码逻辑。实现的方法是:写一个心跳检测器的实现类并实例化,再写一个适配器实现节拍器模型的接口,把心跳检测器的对象传入适配器,在适配器的方法下可以实际调用“心跳”相关的方法,然后对于一些不需要的方法就可以不去实现。然后是控制器部分,可以让心跳控制器实现节拍控制器的接口,把心跳的model传入构造方法,然后用适配器包装后就可以传入视图使用了,至于其中的方法,选择性实现就可以了。