bboyjing's blog

HeadFirst设计模式十五【复合模式之MVC模式】

本章节我们将来学习复合模式的典范:MVC模式,也就是我们一直在用的视图、模型、控制器。将剖析其内部运作细节,从而进一步了解到复合模式的好处。

场景描述

书中描述了一个MP3播放器的场景,比如说iTune。可以用它的界面加入新的歌曲、管理播放清单、将歌曲改名。播放器有一个小型数据库,记录所有的歌曲合相关的名字合数据。播放器也可以播歌,而播歌时用户界面会显示当时的歌曲标题、运行时间等信息。MVC模式的流程大致如下:

  1. 假如我们是用户,合视图交互

    视图是模型的窗口。当你对视图做一些事情时(比如说,按下播放按钮),视图就告诉控制器你做了什么。控制器会负责处理。

  2. 控制器要求模型改变状态

    控制器解读你的动作,如果你按下某个按钮,控制器会理解这个动作的意义,并告知模型如何做出对应的动作。

  3. 控制器也可能要求视图做改变

    当控制器从视图接收到某一动作,结果可能是它也需要告诉视图改变其结果。比如说,控制器可以将界面上某些按钮或菜单项变成有效或无效。

  4. 当模型状态改变时, 模型会通知视图

    不管是你做了某些动作(比如说按下按钮)还是内部有了某些改变(比如说播放清单的下一首歌开始),只要当模型内的东西改变时,模型都会通知视图它的状态改变了。

  5. 视图向模型询问状态

    视图直接从模型取得它的状态。比如说,当模型通知视图新歌开始播放,视图向模型询问歌名并显示出来。当控制器请求视图改变时,视图也可能向模型询问某些状态。

代码示例

这个模式示例代码有点多,一下子看了可能会有点懵,下面一点点抽丝剥茧,将M、V、C的每个部分都实现出来。整个MVC模式会用到策略模式、观察者模式、组合模式等。

  1. 创建节拍观察者接口、BPM观察者接口。所属于View模块,都只有一个更新接口

    1
    2
    3
    public interface BeatObserver {
    void updateBeat();
    }
    1
    2
    3
    public interface BPMObserver {
    void updateBPM();
    }
  2. 创建控制器接口,所属于Controller模块,用于开始或停止播放,以及调整BPM

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    public interface ControllerInterface {
    void start();
    void stop();
    void increaseBPM();
    void decreaseBPM();
    void setBPM(int bpm);
    }
  3. 创建模型层接口,模型是负责维护所有的数据、状态合应用逻辑的。所属于Model模块,是观察者模式中的被观察者

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    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);
    }
  4. 创建BeatModel,这是底层数据存储结构,相对会复杂一点,所属于Model模块。这里大量代码涉及到javax.sound.midi包中的内容,这部分不深究,直接使用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    public class BeatModel implements BeatModelInterface, MetaEventListener {
    Sequencer sequencer;
    Sequence sequence;
    ArrayList<BeatObserver> beatObservers = new ArrayList<>();
    ArrayList<BPMObserver> bpmObservers = new ArrayList<>();
    int bpm = 90;
    Track track;
    /**
    * 初始化,其中方法绝大部分与javax.sound.midi有关
    */
    @Override
    public void initialize() {
    setUpMidi();
    buildTrackAndStart();
    }
    /**
    * 启动节拍器
    */
    @Override
    public void on() {
    System.out.println("Starting the sequencer");
    sequencer.start();
    setBPM(90);
    }
    /**
    * 停止节拍器
    */
    @Override
    public void off() {
    setBPM(0);
    sequencer.stop();
    }
    @Override
    public void setBPM(int bpm) {
    this.bpm = bpm;
    sequencer.setTempoInBPM(getBPM());
    notifyBPMObservers();
    }
    @Override
    public int getBPM() {
    return bpm;
    }
    @Override
    public void registerObserver(BeatObserver o) {
    beatObservers.add(o);
    }
    @Override
    public void removeObserver(BeatObserver o) {
    int i = beatObservers.indexOf(o);
    if (i >= 0) {
    beatObservers.remove(i);
    }
    }
    @Override
    public void registerObserver(BPMObserver o) {
    bpmObservers.add(o);
    }
    @Override
    public void removeObserver(BPMObserver o) {
    int i = bpmObservers.indexOf(o);
    if (i >= 0) {
    bpmObservers.remove(i);
    }
    }
    @Override
    public void meta(MetaMessage meta) {
    if (meta.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());
    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);
    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;
    }
    public void notifyBPMObservers() {
    for (int i = 0; i < bpmObservers.size(); i++) {
    BPMObserver observer = bpmObservers.get(i);
    observer.updateBPM();
    }
    }
    public void notifyBeatObservers() {
    for (int i = 0; i < beatObservers.size(); i++) {
    BeatObserver observer = beatObservers.get(i);
    observer.updateBeat();
    }
    }
    void beatEvent() {
    notifyBeatObservers();
    }
    }
  5. 边写View层实现,所属于View模块,其中大量代码涉及到javax.swing包,不作深究,直接使用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    public class DJView implements ActionListener, BeatObserver, BPMObserver {
    /**
    * 被观察对象 --> 模型层
    */
    BeatModelInterface model;
    /**
    * 策略模式
    * 注入控制策略
    */
    ControllerInterface controller;
    /**
    * javax.swing相关内容
    */
    BeatBar beatBar;
    JFrame viewFrame;
    JPanel viewPanel;
    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);
    }
    @Override
    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());
    }
    }
    }
    }
    @Override
    public void updateBeat() {
    if (beatBar != null) {
    beatBar.setValue(100);
    }
    }
    @Override
    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 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() {
    @Override
    public void actionPerformed(ActionEvent event) {
    controller.start();
    }
    });
    stopMenuItem = new JMenuItem("Stop");
    menu.add(stopMenuItem);
    stopMenuItem.addActionListener(new ActionListener() {
    @Override
    public void actionPerformed(ActionEvent event) {
    controller.stop();
    }
    });
    JMenuItem exit = new JMenuItem("Quit");
    exit.addActionListener(new ActionListener() {
    @Override
    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);
    }
    }

    同时额外新增一个类,也是swing相关

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    public class BeatBar extends JProgressBar implements Runnable {
    private static final long serialVersionUID = 2L;
    JProgressBar progressBar;
    Thread thread;
    public BeatBar() {
    thread = new Thread(this);
    setMaximum(100);
    thread.start();
    }
    @Override
    public void run() {
    for (; ; ) {
    int value = getValue();
    value = (int) (value * .75);
    setValue(value);
    repaint();
    try {
    Thread.sleep(50);
    } catch (Exception e) {
    }
    }
    }
    }
  6. 实现控制器逻辑,所属于Congroller模块

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    public class BeatController implements ControllerInterface {
    /**
    * 控制器同时注入Model和View
    */
    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() {
    model.on();
    view.disableStartMenuItem();
    view.enableStopMenuItem();
    }
    @Override
    public void stop() {
    model.off();
    view.disableStopMenuItem();
    view.enableStartMenuItem();
    }
    @Override
    public void increaseBPM() {
    int bpm = model.getBPM();
    model.setBPM(bpm + 1);
    }
    @Override
    public void decreaseBPM() {
    int bpm = model.getBPM();
    model.setBPM(bpm - 1);
    }
    @Override
    public void setBPM(int bpm) {
    model.setBPM(bpm);
    }
    }
  7. 测试

    1
    2
    3
    4
    5
    6
    public class Test {
    public static void main(String[] args) {
    BeatModelInterface model = new BeatModel();
    ControllerInterface controller = new BeatController(model);
    }
    }

    我测试了下,节拍没那么准确,这个不重要了,本章的目的在于剖析复合模式是如何变成MVC模式中的。我们看到了组合模式、策略模式、观察者模式之间的配合。日后在实际项目中,我们也可以根据需求创造出自己的复合模式来。本章节就到这里了。