相信有过互联网相关开发经验的肯定听说过MVC的大名。
简单地说,MVC即:
-
Model:模型
-
View:视图
-
Controller:控制器
视图很明确,一般就是可视化的部分,视应用种类的不同可能是UI控件,也可能是HTML元素。
控制器夹再视图和模型中间,起一个承上启下的作用,向上,它直接提供视图需要的接口,向下它调用模型的相关服务来完成接口的具体逻辑。
一般来说视图和控制器之间的界限是很明确的,但控制器和模型中间往往会有一些灰色的地带,可能有些对数据的操作逻辑可以放在模型层,也可以放在控制器层,对此我的建议是如果该逻辑会在控制器层中多次调用,出于复用的目的肯定需要放在模型层中,如果并不会多次调用,只是具体的某些视图下的接口需要,则可以放在控制器层。
这里我们使用《Head First 设计模式》中最后一个章节的示例进行说明。
节拍器
最后一个章节的示例程序是一个swing控件开发的节拍器,其UI界面长这样:
由两个窗口组成,一个负责设置节拍相关参数以及启动和关闭节拍器,另一个则显示节拍。
模型
这里先定义模型层的接口:
package xyz.icexmoon.beat_tool.model;
import xyz.icexmoon.beat_tool.common.BPMObserver;
import xyz.icexmoon.beat_tool.common.BeatObserver;
public interface BeatModelInterface {
public void init();
public void on();
public void off();
public void setBPM(int bpm);
public int getBPM();
public void registeObserver(BeatObserver observer);
public void registeObserver(BPMObserver observer);
public void removeObserver(BeatObserver observer);
public void removeObserver(BPMObserver observer);
}
模型层的接口提供了一些控制层需要用到的方法,比如开启(on
)、关闭节拍器(off
),初始化节拍器(init
),设置节拍频率setBPM
,获取节拍频率(getBPM()
)。
除此之外,还由注册和删除观察者的方法,这正是观察者模式的应用。这里使用观察者模式的意图在于我们其中一个界面中的进度条需要实时显示节拍,如果节拍器的频率改变,则进度条的变化频率也要改变,所以如果进度条可以通过观察者模式“订阅”节拍器模型,则就可以在模型中的频率改变时通知到视图的相关组件,进而调整进度条的刷新频率。
而我们这里需要设置两种观察者,一种用于订阅节拍器的频率变化,一种用于订阅每次节拍的“跳动”。前者使用BPMObserver
表示,后者使用BeatObserver
。
当然这里也可以设置一种观察者接口来实现,只不过这样就不能用方法重载了,接口中的相关方法就需要改为
registeBeatObserver(Observer o)
这样。
相应的观察者接口很简单:
package xyz.icexmoon.beat_tool.common;
public interface BeatObserver {
public void notifyObserver();
}
这里不能是
notify
,因为Object
有方法名称就是notify
。
具体的模型层实现:
package xyz.icexmoon.beat_tool.model;
import java.util.*;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.Clip;
import java.io.*;
import javax.sound.sampled.Line;
import xyz.icexmoon.beat_tool.common.BPMObserver;
import xyz.icexmoon.beat_tool.common.BeatObserver;
public class BeatModel2 implements Runnable, BeatModelInterface {
List<BeatObserver> beatObservers = new ArrayList<BeatObserver>();
List<BPMObserver> bpmObservers = new ArrayList<BPMObserver>();
int bpm = 90;
Thread thread;
boolean stop = false;
Clip clip;
public void init() {
try {
File resource = new File("clap.wav");
clip = (Clip) AudioSystem.getLine(new Line.Info(Clip.class));
clip.open(AudioSystem.getAudioInputStream(resource));
} catch (Exception ex) {
System.out.println("Error: Can't load clip");
System.out.println(ex);
}
}
public void on() {
bpm = 90;
// notifyBPMObservers();
thread = new Thread(this);
stop = false;
thread.start();
}
public void off() {
stopBeat();
stop = true;
}
public void run() {
while (!stop) {
playBeat();
notifyBeatObservers();
try {
Thread.sleep(60000 / getBPM());
} catch (Exception e) {
}
}
}
public void setBPM(int bpm) {
this.bpm = bpm;
notifyBPMObservers();
}
public int getBPM() {
return bpm;
}
public void notifyBeatObservers() {
for (int i = 0; i < beatObservers.size(); i++) {
BeatObserver observer = (BeatObserver) beatObservers.get(i);
observer.notifyObserver();
}
}
public void notifyBPMObservers() {
for (int i = 0; i < bpmObservers.size(); i++) {
BPMObserver observer = (BPMObserver) bpmObservers.get(i);
observer.notifyObserver();
}
}
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 playBeat() {
clip.setFramePosition(0);
clip.start();
}
public void stopBeat() {
clip.setFramePosition(0);
clip.stop();
}
public void registeObserver(BeatObserver observer) {
this.beatObservers.add(observer);
}
public void registeObserver(BPMObserver observer) {
this.bpmObservers.add(observer);
}
}
这里的具体实现和原书不同,原书的实现在我调试时候不起作用,原因不清楚。这里的实现参考了Github上的这个项目。
视图
接着我们创建视图:
package xyz.icexmoon.beat_tool.view;
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import xyz.icexmoon.beat_tool.common.BPMObserver;
import xyz.icexmoon.beat_tool.common.BeatObserver;
import xyz.icexmoon.beat_tool.control.ControllerInterface;
import xyz.icexmoon.beat_tool.model.BeatModelInterface;
public class DJView implements ActionListener {
DJBeatObserver beatObserver = new DJBeatObserver();
DJBPMObserver bpmobserver = new DJBPMObserver();
private BeatModelInterface model;
private ControllerInterface controller;
private JPanel viewPanel;
private JFrame viewFrame;
private BeatBar beatBar;
private JLabel bpmOutputLabel;
private JFrame controlFrame;
private JPanel controlPanel;
private JMenuBar menuBar;
private JMenu menu;
private JMenuItem startMenuItem;
private JMenuItem stopMenuItem;
private JTextField bpmTextField;
private JLabel bpmLabel;
private JButton setBPMButton;
private JButton increaseBPMButton;
private JButton decreaseBPMButton;
public DJView(ControllerInterface controllerInterface, BeatModelInterface beatModelInterface) {
this.controller = controllerInterface;
this.model = beatModelInterface;
beatModelInterface.registeObserver(beatObserver);
beatModelInterface.registeObserver(bpmobserver);
}
public void createView() {
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);
}
public void createControls() {
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 = new JMenuBar();
menu = new JMenu("DJ Control");
startMenuItem = new JMenuItem("Start");
menu.add(startMenuItem);
startMenuItem.addActionListener((event) -> controller.start());
stopMenuItem = new JMenuItem("Stop");
menu.add(stopMenuItem);
stopMenuItem.addActionListener((event) -> controller.stop());
JMenuItem exit = new JMenuItem("Quit");
exit.addActionListener((event) -> System.exit(0));
menu.add(exit);
menuBar.add(menu);
controlFrame.setJMenuBar(menuBar);
bpmTextField = new JTextField(2);
bpmLabel = new JLabel("Enter BPM:", SwingConstants.RIGHT);
setBPMButton = new JButton("Set");
setBPMButton.setSize(new Dimension(10, 40));
increaseBPMButton = new JButton(">>");
decreaseBPMButton = new JButton("<<");
setBPMButton.addActionListener(this);
increaseBPMButton.addActionListener(this);
decreaseBPMButton.addActionListener(this);
JPanel buttonPanel = new JPanel(new GridLayout(1, 2));
buttonPanel.add(decreaseBPMButton);
buttonPanel.add(increaseBPMButton);
JPanel enterPanel = new JPanel(new GridLayout(1, 2));
enterPanel.add(bpmLabel);
enterPanel.add(bpmTextField);
JPanel insideControlPanel = new JPanel(new GridLayout(3, 1));
insideControlPanel.add(enterPanel);
insideControlPanel.add(setBPMButton);
insideControlPanel.add(buttonPanel);
controlPanel.add(insideControlPanel);
bpmLabel.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
bpmOutputLabel.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
controlFrame.getRootPane().setDefaultButton(setBPMButton);
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);
}
class DJBeatObserver implements BeatObserver {
public void notifyObserver() {
if (beatBar != null) {
beatBar.setValue(100);
}
}
}
class DJBPMObserver implements BPMObserver {
public void notifyObserver() {
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 actionPerformed(ActionEvent event) {
if (event.getSource() == setBPMButton) {
int bpm = 90;
String bpmText = bpmTextField.getText();
if (bpmText == null || bpmText.contentEquals("")) {
bpm = 90;
} else {
bpm = Integer.parseInt(bpmTextField.getText());
}
controller.setBPM(bpm);
} else if (event.getSource() == increaseBPMButton) {
controller.increaseBPM();
} else if (event.getSource() == decreaseBPMButton) {
controller.decreaseBPM();
}
}
}
视图的大部分代码都用于创建两个窗口,此外就是相关按钮的监听器设置,还有之前说过的观察者。
其实常规做法是让视图直接实现相应的观察者接口,然后调用相应的注册方法注册到模型,但这里我们两种观察者接口中的方法名称相同,显然就不能那么做了,所以这里使用内部类的方式实现。
最后要做的就是创建控制器,将模型和视图粘合起来。
控制器
我们这里对控制器同样创建了接口,这样实现相同接口的控制器可以在视图中进行混用,灵活性更高。
package xyz.icexmoon.beat_tool.control;
public interface ControllerInterface {
void start();
void stop();
void setBPM(int bpm);
void increaseBPM();
void decreaseBPM();
}
具体的控制器实现:
package xyz.icexmoon.beat_tool.control;
import xyz.icexmoon.beat_tool.model.BeatModelInterface;
import xyz.icexmoon.beat_tool.view.DJView;
public class BeatController implements ControllerInterface {
private BeatModelInterface model;
private DJView view;
public BeatController(BeatModelInterface model) {
this.model = model;
view = new DJView(this, model);
view.createView();
view.createControls();
view.disableStopMenuItem();
view.enableStartMenuItem();
model.init();
}
public void start() {
model.on();
view.disableStartMenuItem();
view.enableStopMenuItem();
}
public void stop() {
model.off();
view.disableStopMenuItem();
view.enableStartMenuItem();
}
public void setBPM(int bpm) {
model.setBPM(bpm);
}
public void increaseBPM() {
int bpm = model.getBPM();
model.setBPM(bpm + 1);
}
public void decreaseBPM() {
int bpm = model.getBPM();
model.setBPM(bpm - 1);
}
}
可以看到主要工作在模型和视图,现实中也是如此,大多数MVC的互联网项目中控制器的任务都是最少的,最主要的工作反而是数据合法性验证和用户验证,主要工作都在视图和模型。
之所以说MVC是一个使用广泛的组合模式,是因为其可以广泛应用于具有图形界面的应用开发的同时具有以下优点:
-
明确了模型-控制器-视图的职责和功能,让代码结构变得明确清晰,让团队可以很容易地进行分工。
-
通过使用观察者模式,视图可以在模型改变的时候获得通知,进而做出相应的改变,这在开发者的角度看来无疑视图显得更“智能”,无需大量的其它代码干预这种行为。
-
视图层本身会以组合模式进行组织,这点在swing框架或者安卓开发中显得尤为明显,视图的绘制过程是从顶层的
JFrame
控件一直往下嵌套调用所包含的UI组件的绘制方法,你只需要调用顶层控件的绘制方法就可以绘制所有的UI组件。当然这点在web开发中没有体现。 -
控制器可以是视图的策略,模型也可以是控制器的策略,通过使用策略模式统一相关控制器或者视图的接口,就可以在一组控制器或者视图中实现相互替换,这让用同一视图通过替换不同的控制器实现不同的功能成为可能。
关于使用最广泛最具有代表性的复合模式MVC到这里就介绍完毕了,谢谢阅读。
本文中的完整代码示例见Github仓库
文章评论