1. 程式人生 > >設計模式十二之MVC模式(Java)

設計模式十二之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不能選擇,其他兩個可以選擇

點選停止選單項,同時可以看到被禁止重新選擇,其他兩個可以選擇。