設計模式十二之MVC模式(Java)
這是我看Head first設計模式書籍之後想要總結的知識點,一方面是對自己學習的東西總結和提煉加強自己的理解和記憶,另一方面是給大家簡化這本書,方便大家快速瞭解各種設計模式。
我想提醒大家的是,設計模式只是前人總結的一些經驗套路,實際上還是要在開發專案中慢慢體會,不可成為設計模式的中毒患者,強行照搬設計模式的一些規則。
上節我們簡單講解了MVC的基本概念,現在我們以一個實際的Java GUI 播放MIDI音樂程式案例來進一步瞭解MVC模式
先說說我們程式中檢視,控制器和模型的作用
檢視,用來呈現模型。檢視通常直接從模型中取得它需要顯示的狀態和資料(模型提供介面檢視呼叫就行了,一般getter方法等即可),檢視持有模型和控制器的引用,這樣可以呼叫他們的方法。檢視實現了觀察者介面,是觀察者,模型中可以註冊觀察者,觀察模型BPM和Beat的變化,檢視再根據變化改變介面。
控制器: 使用者在檢視上進行點選事件觸發,呼叫控制器對應的方法來讓控制器進一步解讀。控制器的作用就是解耦模型和檢視。
模型: 模型持有所有的資料、狀態和程式邏輯。模型沒有注意到檢視和控制器,雖然它提供了操縱和檢索狀態的介面,併發送狀態改變通知給觀察者(檢視)。
使用了哪些模式?
組合模式: 檢視顯示了包括視窗、面板、按鈕、文字標籤等。每個顯示元件如果不是組合結點(如視窗、面板),就是葉節點(如按鈕).當控制器告訴檢視更新時,只需告訴檢視最頂層的元件即可,組合會處理其餘的事。
策略模式: 檢視和控制器實現了經典的策略模式,檢視是一個物件,可以被調整使用不同的策略,而控制器提供了策略。檢視只關心繫統中可視的部分,對於任何介面行為,都委託給控制器處理。使用策略模式也可以讓檢視和模型之間的關係解耦,因為控制器負責和模型互動來傳遞使用者的請求。對於工作如何完成的,檢視毫不知情。控制器有控制器介面,而由具體控制器實現這個介面形成演算法簇,這樣體現了策略模式。
觀察者模式: 模型實現了觀察者模式,當狀態改變時,相關物件將持續更新。使用觀察者模式,可以讓模型完全獨立於檢視和控制器。同一個模型可以使用不同的檢視,甚至可以同時使用多個檢視。
我們看程式碼部分
模型介面BeatModelInterface.java
package headfirst.designpatterns.combined.djview; /* 模型使用觀察者模式,我們需要讓物件註冊成為觀察者併發送出通知 */ 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); // 註冊成為BPM觀察者 void removeObserver(BPMObserver o); }
模型具體實現BeatModel.java
package com.gougoucompany.designpattern.mvc;
import java.util.ArrayList;
import java.util.Iterator;
import javax.sound.midi.InvalidMidiDataException;
import javax.sound.midi.MetaEventListener;
import javax.sound.midi.MetaMessage;
import javax.sound.midi.MidiEvent;
import javax.sound.midi.MidiSystem;
import javax.sound.midi.Sequence;
import javax.sound.midi.Sequencer;
import javax.sound.midi.ShortMessage;
import javax.sound.midi.Track;
import javax.xml.ws.handler.MessageContext;
/*
模型: 持有所有的資料、狀態和程式邏輯
The MetaEventListener interface should be implemented by classes whose instances need to be notified when a Sequencer
has processed a MetaMessage. To register a MetaEventListener object to receive such notifications, pass it as the
argument to the addMetaEventListener method of Sequencer.
Sequencer處理源資訊時,BeatModel例項需要被通知
void meta(MetaMessage meta) 這個介面只有一個抽象方法,
Invoked when a Sequencer has encountered and processed a MetaMessage in the Sequence it is processing.
*/
public class BeatModel implements BeatModelInterface, MetaEventListener {
Sequencer sequencer;
ArrayList<BeatObserver> beatObservers = new ArrayList<>();
ArrayList<BPMObserver> bpmObservers = new ArrayList<>();
int bpm = 90;
Sequence sequence;
Track track;
@Override
public void initialize() {
// TODO Auto-generated method stub
setUpMidi();
buildTrackAndStart();
}
public void setUpMidi() {
try {
sequencer = MidiSystem.getSequencer();
sequencer.open();
sequencer.addMetaEventListener(this);
sequence = new Sequence(Sequence.PPQ, 4);
track = sequence.createTrack();
sequencer.setTempoInBPM(getBPM());
sequencer.setLoopCount(Sequencer.LOOP_CONTINUOUSLY);
} catch(Exception e){
e.printStackTrace();
}
}
public void buildTrackAndStart() {
int[] trackList = {35, 0, 46, 0};
sequence.deleteTrack(null);
track = sequence.createTrack();
makeTracks(trackList);
MetaMessage metaMessage = new MetaMessage();
String text = "a metaEvent maker";
try {
metaMessage.setMessage(47, text.getBytes(), text.length());
} catch (InvalidMidiDataException e1) {
e1.printStackTrace();
}
track.add(new MidiEvent(metaMessage, 3)); //這裡是為了產生MetaEvent事件,Type設定為47,這樣脈動柱進度條就可以100->0 100->0的動態形式
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 shortMessage = new ShortMessage();
shortMessage.setMessage(comd, chan, one ,two);
event = new MidiEvent(shortMessage, tick);
} catch (Exception e) {
e.printStackTrace();
}
return event;
}
@Override
public void on() {
// TODO Auto-generated method stub
System.out.println("Starting the sequencer");
sequencer.start();
setBPM(90);
}
@Override
public void off() {
// TODO Auto-generated method stub
setBPM(0);
sequencer.stop();
}
@Override
public void setBPM(int bpm) {
// TODO Auto-generated method stub
this.bpm = bpm;
sequencer.setTempoInBPM(bpm);
notifyBPMObservers();
}
@Override
public int getBPM() {
// TODO Auto-generated method stub
return bpm;
}
//新的節拍開始,通知觀察者
void beatEvent() {
notifyBeatObservers();
}
@Override
public void registerObserver(BeatObserver o) {
// TODO Auto-generated method stub
beatObservers.add(o);
}
@Override
public void removeObserver(BeatObserver o) {
// TODO Auto-generated method stub
int i = beatObservers.indexOf(o); //元素不存在會返回-1
if(i >= 0) {
beatObservers.remove(i);
}
}
public void notifyBeatObservers() {
//可以使用三種基本的遍歷方法
Iterator<BeatObserver> iterator = beatObservers.iterator();
while(iterator.hasNext()) {
BeatObserver observer = (BeatObserver)iterator.next();
observer.updateBeat();
}
// for(BeatObserver observer : beatObservers) {
// observer.updateBeat(); //for-each 使用的還是內建的迭代器
// }
//
// for(int i = 0; i < beatObservers.size(); i ++) {
// BeatObserver observer = beatObservers.get(i);
// observer.updateBeat();
// }
}
@Override
public void registerObserver(BPMObserver o) {
// TODO Auto-generated method stub
bpmObservers.add(o);
}
@Override
public void removeObserver(BPMObserver o) {
// TODO Auto-generated method stub
int i = bpmObservers.indexOf(o);
if(i >= 0) {
bpmObservers.remove(i);
}
}
public void notifyBPMObservers() {
Iterator<BPMObserver> iterator = bpmObservers.iterator();
while(iterator.hasNext()) {
BPMObserver observer = iterator.next();
observer.updateBPM();
}
}
@Override
public void meta(MetaMessage message) {
// TODO Auto-generated method stub
if(message.getType() == 47) {
beatEvent(); //節拍改變通知觀察者
sequencer.start();
setBPM(getBPM()); //bpm可能改變,通知所有的觀察者
}
}
}
檢視
我們實現要用兩個分離的視窗;一個視窗包含當前的BPM和脈動柱(把模型的檢視從控制的檢視分離),另一個則包含介面控制。
檢視實現了兩個觀察者介面,對模型BPM和Beat節拍的觀察
BeatObserver.java
package com.gougoucompany.designpattern.mvc;
//節拍觀察者介面
public interface BeatObserver {
void updateBeat();
}
BPMObserver.java
package com.gougoucompany.designpattern.mvc;
//BPM觀察者介面
public interface BPMObserver {
void updateBPM();
}
檢視DJView.java
package com.gougoucompany.designpattern.mvc;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.GridLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
//DJView是一個觀察者,同時關心實時節拍和BPM的改變
import javax.swing.BorderFactory;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JMenu;
import javax.swing.JMenuBar;
import javax.swing.JMenuItem;
import javax.swing.JPanel;
import javax.swing.JTextField;
import javax.swing.SwingConstants;
public class DJView implements ActionListener, BeatObserver, BPMObserver {
//檢視持有模型和控制器的引用
BeatModelInterface model;
ControllerInterface controller;
//包含模型檢視介面的元件
JFrame viewFrame;
JPanel viewPanel;
BeatBar beatBar;
JLabel bpmOutputLabel;
//包含其他使用者控制的介面的元件
JFrame controlFrame;
JPanel controlPanel;
JLabel bpmLabel; //bpm顯示標籤
JTextField bpmTextField; //bpm文字框
JButton setBPMButton; //設定bpm按鈕
JButton increaseBPMButton; //增加bpm按鈕
JButton decreaseBPMButton; //減少bpm按鈕
JMenuBar menuBar; //選單欄
JMenu menu; //選單
JMenuItem startMenuItem; //開始選單項
JMenuItem stopMenuItem; //停止選單項
public DJView(ControllerInterface controller, BeatModelInterface model) {
this.controller = controller;
this.model = model;
//註冊成為BeatObserver和BPMObserver觀察者
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(500, 400));
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.setSize(600, 400);
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(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
controller.start(); ////控制器呼叫start方法,模型開始控制播放音樂過程的邏輯
}
});
stopMenuItem = new JMenuItem("Stop");
menu.add(stopMenuItem); //新增停止選單項
stopMenuItem.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
// TODO Auto-generated method stub
controller.stop();
}
});
JMenuItem exit = new JMenuItem("Quit"); //新增退出選單
exit.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
// TODO Auto-generated method stub
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("<<");
//註冊本類監聽set,<<,>>點選事件的監聽
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.setSize(800, 400);
controlFrame.setVisible(true);
}
/*
模型BPM狀態改變時,updateBPM()方法會被呼叫。
這時候我們更新當前BPM的顯示。我們可以通過直接請求模型而得到這個值
*/
@Override
public void updateBPM() {
// TODO Auto-generated method stub
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());
}
}
}
}
/**
* 模型開始一個新的節拍時,updateBeat()方法會被呼叫。這時候,我們讓脈動柱跳一下,我們設定
* 為100,讓脈動柱自行處理動畫部分。
*/
@Override
public void updateBeat() {
// TODO Auto-generated method stub
if(beatBar != null) {
beatBar.setValue(100);
}
}
/*
* 這些方法將選單中的start和stop項變成enable或disable,
* 控制器可以利用這些介面方法改變使用者介面
*/
public void enableStopMenuItem() {
stopMenuItem.setEnabled(true);
}
public void disableStopMenuItem() {
stopMenuItem.setEnabled(false);
}
public void enableStartMenuItem() {
startMenuItem.setEnabled(true);
}
public void disableStartMenuItem() {
startMenuItem.setEnabled(false);
}
//不同按鈕被單擊,將資訊傳給控制器來進一步改變模型的
@Override
public void actionPerformed(ActionEvent e) {
// TODO Auto-generated method stub
if(e.getSource() == setBPMButton) {
//如果bpmTextField不為空,並且內容不是空字串
if(bpmTextField != null && !bpmTextField.getText().isEmpty()) {
int bpm = Integer.parseInt(bpmTextField.getText());
controller.setBPM(bpm);
}
} else if(e.getSource() == increaseBPMButton) {
controller.increaseBPM();
} else if(e.getSource() == decreaseBPMButton) {
controller.decreaseBPM();
}
}
}
脈動柱元件繼承了JProcessBar,又實現了Runnable介面,因為我們希望他的進度可以自身實時變化(當模型Sequencer處理MetaMessage的時候,就會觸發實現的meta()方法,來通知檢視來進行對脈動柱進度的設定。這樣脈動柱就會一直從最大Max100到接近0不斷變化跳動)
BeatBar.java
package com.gougoucompany.designpattern.mvc;
import javax.swing.JProgressBar;
//脈動柱元件
public class BeatBar extends JProgressBar implements Runnable{
Thread thread;
public BeatBar() {
thread = new Thread(this);
//設定最大值,繼承下來的方法
setMaximum(100);
thread.start();
}
//執行緒的run方法是一個死迴圈,因此始終監聽脈動柱的數值變化,來進行進度條的設定
@Override
public void run() {
for(;;) {
int value = getValue();
System.out.println("進度條的值是:" + value);
value = (int)(value * .75); //從設定的值開始縮減到接近0
setValue(value);
repaint();
try {
Thread.sleep(50);//每50毫秒更新一次
} catch (Exception e) {
// TODO: handle exception
e.printStackTrace();
}
}
}
}
控制器是策略
控制器介面ControllInterface.java
package com.gougoucompany.designpattern.mvc;
/*
控制器是策略,我們將控制器傳入檢視的構造器中,
檢視可以呼叫下面所有的方法,實現了ControllerInterface就是一系列演算法簇,
這樣沒有依賴,可以隨時替換不同控制器的實現類
*/
public interface ControllerInterface {
void stop();
void increaseBPM();
void decreaseBPM();
void setBPM(int bpm);
void start();
}
控制器
package com.gougoucompany.designpattern.mvc;
public class BeatController implements ControllerInterface {
/*
控制器在MVC設計中起到橋樑的作用,所以它擁有模型和檢視的例項
*/
BeatModelInterface model;
DJView view;
public BeatController(BeatModelInterface model) {
this.model = model;
//建立例項並初始化檢視和模型
view = new DJView(this, model);
view.createView();
view.createControls();
view.disableStopMenuItem();
view.enableStartMenuItem();
model.initialize();
}
@Override
public void start() {
// TODO Auto-generated method stub
model.on();
view.disableStartMenuItem();
view.enableStopMenuItem();
}
@Override
public void stop() {
// TODO Auto-generated method stub
model.off();
view.disableStopMenuItem();
view.enableStartMenuItem();
}
@Override
public void increaseBPM() {
// TODO Auto-generated method stub
int bpm = model.getBPM();
model.setBPM(bpm + 1);
}
@Override
public void decreaseBPM() {
// TODO Auto-generated method stub
int bpm = model.getBPM();
model.setBPM(bpm - 1);
}
@Override
public void setBPM(int bpm) {
// TODO Auto-generated method stub
model.setBPM(bpm);
}
}
測試類
package com.gougoucompany.designpattern.mvc;
public class DJTestDrive {
public static void main(String args[]) {
BeatModelInterface model = new BeatModel();
ControllerInterface controller = new BeatController(model);
}
}
下面是程式執行圖:
這是程式一開始執行,初始化的時候圖,此時左邊提示offline不線上
在DJControl選單欄點選展開選單,選單中點選選單項Start,可以看到BPM為90,脈動柱持續跳動
在文字標籤框輸入200,點選set按鈕,左邊檢視變為200,同時脈動柱持續跳動
下面是點選Start選單項之後,可以看到Start不能選擇,其他兩個可以選擇
點選停止選單項,同時可以看到被禁止重新選擇,其他兩個可以選擇。