作者:gnuhpc
出處:http://www.cnblogs.com/gnuhpc/
1.M-V-C ——Model--View--Controller,模式-視圖-控制器,這是一種范型。模型對象正是應用系統存在的理由,你設計的對象,包含了數據、邏輯和其他在你的應用領域創建定制的類。視圖通常是控件,用來顯示和編輯,控制器位於二者中間,負責將每個改變的狀態送進送出。而學習設計模式是理解MVC的鑰匙。書中用一個iTunes的例子直觀描述了MVC:
2.MVC的基本原理:
- 視圖:用來呈現模型。視圖通常直接從模型中取得它需要顯示的數據。 視圖不會直接操作模型。
- 控制器:取得用戶的輸入並解讀其對模型的意思。 控制器不會實現應用邏輯,它為視圖實現行為,將視圖傳過來的行為轉化為模型上的動作。它只負責決定調用哪一個模型。
- 模型:持有所有的數據,狀態和程序邏輯。模型沒有注意到視圖和控制器,雖然它提供了操縱和檢索狀態的接口,並且發送狀態改變通知觀察者。 模型只知道有一些觀察者它需要通知,並且提供一些接口供視圖和控制器獲得並設置狀態。
他們三者的交互如下圖:
這里充分體現了我們“單一職責”的這個原則。
3.MVC模式分析:
1)視圖和控制器實現了經典的策略模式:視圖是一個對象,可以被調整使用不同的策略。視圖只關心顯示,而其行為的控制則都使用控制器進行。這樣一來,視圖和模型之間也完成了解耦,因為控制器負責和模型進行用戶請求的交互。
2)視圖中的顯示中包含了很多的要素,這就用到了組合模式,當控制器告訴視圖更新時,只需告訴視圖最頂層的組件即可,組合會處理其余的事。
3)模型則實現了觀察者模式,當狀態改變時,相關對象將持續更新。
4.MVC實例——DJ View
這是一個控制節拍(BPM,每分鍾XX拍)並產生鼓聲的工具。下邊是這個系統的核心,他負責根據節拍(可以設置可以讀取)產生鼓聲——模型:
我們先看看模型的接口:
public interface BeatModelInterface {
void initialize();
void on();
void off();
void setBPM(int bpm);
int getBPM();
void registerObserver(BeatObserver o);//有兩種觀察者,一種觀察者希望每個節拍都被通知,另一種觀察者希望BPM改變時被通知
void removeObserver(BeatObserver o);
void registerObserver(BPMObserver o);
void removeObserver(BPMObserver o);
}
根據這個接口,我們可以實現模型:
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;
}
}
我們現在要把視圖掛上去,讓這個模型可視化!BeatModel對視圖一無所知,我們利用觀察者模式當狀態改變時,只要是注冊為觀察者的視圖都會收到通知。而視圖使用模型的API訪問狀態。
public class DJView implements ActionListener, BeatObserver, BPMObserver {//同時關心時時節拍和BPM的改變
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
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() {
// Create all Swing components here
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(new ActionListener() {
public void actionPerformed(ActionEvent event) {
controller.start();//視圖的點擊觸發控制器的事件
}
});
stopMenuItem = new JMenuItem("Stop");
menu.add(stopMenuItem);
stopMenuItem.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent event) {
controller.stop();
}
});
JMenuItem exit = new JMenuItem("Quit");
exit.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent 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);
}
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);
}
}
}
有了視圖,有了模型,我們要構建控制器,使得視圖更加聰明,我們使用策略模式,從控制器接口開始設計:
public interface ControllerInterface {
void start();
void stop();
void increaseBPM();
void decreaseBPM();
void setBPM(int bpm);
}
根據這個接口,我們實現這個控制器:
public class BeatController implements ControllerInterface {
BeatModelInterface model;//MVC中,控制器在中間,所以要同時持有模型以及視圖的引用。
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() {//控制器在得到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);
}
}
搞定!我們寫一段測試代碼來使用我們自己的MVC,先創建一個模型,然后創建一個控制器,將模型傳入其中,控制器創建視圖:
public class DJTestDrive {
public static void main (String[] args) {
BeatModelInterface model = new BeatModel();
ControllerInterface controller = new BeatController(model);
}
}
5.我們現在利用這個MVC模型完成另一項工作:心臟監視。我們希望將HeartModel適配成BeatModel
首先我們更換一下模型:
public interface HeartModelInterface {
int getHeartRate();
void registerObserver(BeatObserver o);
void removeObserver(BeatObserver o);
void registerObserver(BPMObserver o);
void removeObserver(BPMObserver o);
}
此時,我們需要知道視圖只知道getBPM而不知道getHeartRate,那么這就需要我們使用適配器模式進行適配了。這就引出了一個MVC中重要的技巧:
使用適配器將模型適配成符合現有視圖和控制器的需要的模型。
public class HeartAdapter implements BeatModelInterface {//適配器要對被適配的接口進行實現,也就是那個在Client中被直接使用的部分
HeartModelInterface heart;//適配器中要保留另一部分的引用
public HeartAdapter(HeartModelInterface heart) {
this.heart = heart;
}
public void initialize() {}//不需要的部分我們在適配器中留空。
public void on() {}
public void off() {}
public int getBPM() {
return heart.getHeartRate();//適配器在此處運轉
}
public void setBPM(int bpm) {}
public void registerObserver(BeatObserver o) {//將注冊觀察者Server的方法委托給heart
heart.registerObserver(o);
}
public void removeObserver(BeatObserver o) {
heart.removeObserver(o);
}
public void registerObserver(BPMObserver o) {
heart.registerObserver(o);
}
public void removeObserver(BPMObserver o) {
heart.removeObserver(o);
}
}
適配器ready以后,我們可以完成控制器了:
public class HeartController implements ControllerInterface {
HeartModelInterface model;
DJView view;
public HeartController(HeartModelInterface model) {
this.model = model;
view = new DJView(this, new HeartAdapter(model)); //用適配器進行包裝
view.createView();
view.createControls();
view.disableStopMenuItem();
view.disableStartMenuItem();
}
public void start() {} //沒有實際作用的我們留空
public void stop() {}
public void increaseBPM() {}
public void decreaseBPM() {}
public void setBPM(int bpm) {}
}
我們現在就可以寫一段測試代碼了:
public class HeartTestDrive {
public static void main (String[] args) {
HeartModel heartModel = new HeartModel();//首先創建模型
ControllerInterface model = new HeartController(heartModel);//然后創建控制器,控制器中創建了視圖
}
}
6.最后我們提一句:在Web開發中,MVC被經常叫做Model 2。有了這個模型,該編程的人就去做編程,該做網頁的人就去做網頁。JSP只知道會從控制器收到一個Bean。在這個場景中,其實Bean其實就是模型,而且JSP只用到這個bean的BPM屬性。