【问题标题】:Observer Pattern on MVC for specific fields针对特定领域的 MVC 上的观察者模式
【发布时间】:2019-09-10 02:20:34
【问题描述】:

在 MVC 模式中,这是模型通知视图的最佳选择(如果首先这是正确的方法),在模型存储的所有数据字段中,只有几个字段被更新。特别是当我们只想更新视图的特定字段时。

我目前正在使用带有观察者/订阅者 (JAVA Swing) 的 MVC 模式,如下所述:https://stackoverflow.com/a/6963529 但是当模型更新时,它会在调用 update() 函数时更改视图中的所有内容,因此无法确定为了只更新视图中的必填字段,模型中的哪个字段发生了变化。

我读过这个主题:https://softwareengineering.stackexchange.com/a/359008 和这个:https://stackoverflow.com/a/9815189,我认为它很有用,但是对于后面,我不太明白如何在变量上设置 propertyChangeListener(int,float , 等等)。也与此相关:https://stackoverflow.com/a/9815189

软件开始运行的Main类:

public class Main {
    public static void main(String[] args) {
        Model m = new Model();
        View v = new View(m);
        Controller c = new Controller(m, v);
        c.initController();
    }
}

所以我在模型上的代码是这样的:

public class Model extends Observable {
   //...
   private float speed;
   private int batteryPercentage;

   public float getSpeed() {
       return speed;
   }
   public void setSpeed(float speed) {
       this.speed = speed;
       setChanged();
       notifyObservers();
   }

    public int getBatteryPercentage() {
        return batteryPercentage;
    }
    public void setBatteryPercentage(int batteryPercentage) {
        this.batteryPercentage = batteryPercentage;
        setChanged();
        notifyObservers();
    }
}

视图知道模型:

public class View implements Observer {
    private Model model;
    private JTextField txtFldSpeed;
    private JTextField txtFldBattery;
    private JFrame mainWindow;

    public View(Model m) {
        this.model = m;
        initialize();
    }
    private void initialize() {
        mainWindow = new JFrame();
        mainWindow.setTitle("New Window");
        mainWindow.setMinimumSize(new Dimension(1280, 720));
        mainWindow.setBounds(100, 100, 1280, 720);
        mainWindow.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

        JPanel tPanel1 = new JPanel();
        tPanel1.setBorder(new LineBorder(new Color(0, 0, 0)));
        tPanel1.setLayout(null);
        mainWindow.getContentPane().add(tPanel1);

        mainWindow.getContentPane().add(tPanel1);
        txtFldSpeed = new JTextField();
        txtFldSpeed.setEditable(false);
        txtFldSpeed.setBounds(182, 11, 116, 22);
        tPanel1.add(txtFldSpeed);

        txtFldBattery = new JTextField();
        txtFldBattery.setEditable(false);
        txtFldBattery.setBounds(182, 43, 116, 22);
        tPanel1.add(txtFldBattery);

        mainWindow.setVisible(true);
    }
    @Override
    public void update(Observable o, Object arg) {
        txtFldSpeed.setText(Float.toString(model.getSpeed()) + " kn");
        txtFldBattery.setText(Integer.toString(model.getBatteryPercentage()) + " %");
    }
}

Controller 将 View 添加为 Model 的 Observer:

public class Controller {
    private Model model;
    private View view;

    public Controller(Model m, View v) {
        this.model = m;
        this.view = v;
    }

    public void initController() {    
        model.addObserver(view);
        model.setSpeed(10);
    }
}

我期待的是,当模型更新时,假设函数setSpeed() 被调用,视图被告知她需要在该特定字段上更新自身,而不是每个“可变”字段(如txtFldBattery

我想这样做是因为在视图上,每秒会更新几次字段,并且因为我需要更新视图上的所有内容,JComboBox 不需要经常更新,尝试选择选项时一直关闭。

【问题讨论】:

  • 请澄清您的问题。此外,问题越具体,您发布的minimal reproducible example 代码越好,通常问题的质量越高,答案就越好。旁注:通常视图会在模型中添加侦听器,这些侦听器会在模型更改时触发。您可以使用一个侦听器并简单地更新模型更改的整个视图,或者您可以使用多个侦听器并更有选择性地更新事物,这听起来像是您正在尝试做的事情。任何解决方案的详细信息将取决于您的代码和问题的详细信息。
  • 感谢您的帮助@HovercraftFullOfEels。我将更新一些代码。我不认为它会有用。
  • 我不认为我以前被称为“Ho”,但我被称为更糟(严重)。不过,如果可能,请再次改进您的问题。
  • 不是你的反对者(还),等待你的更新
  • @HovercraftFullOfEels,对不起! :-D 我试图引用你。我更新了帖子。我省略了一些代码以使其更易于阅读,因为变量命名非常简单。谢谢。

标签: java model-view-controller listener observer-pattern


【解决方案1】:

我会使用 SwingPropertyChangeSupport,将模型的每个状态字段设为“绑定属性”,以便可以单独监听每个状态字段。

例如,假设您有一个看起来像这样的模型:

public class MvcModel {
    public static final String SPEED = "speed";
    public static final String BATTERY = "battery";
    public static final int MAX_SPEED = 40;
    private float speed;
    private int batteryPercentage;
    private SwingPropertyChangeSupport pcSupport = new SwingPropertyChangeSupport(this);

    public float getSpeed() {
        return speed;
    }

    public void setSpeed(float speed) {
        float oldValue = this.speed;
        float newValue = speed;
        this.speed = speed;
        pcSupport.firePropertyChange(SPEED, oldValue, newValue);
    }

    public int getBatteryPercentage() {
        return batteryPercentage;
    }

    public void setBatteryPercentage(int batteryPercentage) {
        int oldValue = this.batteryPercentage;
        int newValue = batteryPercentage;
        this.batteryPercentage = batteryPercentage;
        pcSupport.firePropertyChange(BATTERY, oldValue, newValue);
    }

    public void addPropertyChangeListener(PropertyChangeListener listener) {
        pcSupport.addPropertyChangeListener(listener);
    }

    public void removePropertyChangeListener(PropertyChangeListener listener) {
        pcSupport.removePropertyChangeListener(listener);
    }

    public void addPropertyChangeListener(String name, PropertyChangeListener listener) {
        pcSupport.addPropertyChangeListener(name, listener);
    }

    public void removePropertyChangeListener(String name, PropertyChangeListener listener) {
        pcSupport.removePropertyChangeListener(name, listener);
    }

}

speed 和 batteryPercent 字段都是“绑定字段”,因为对这些字段的任何更改都将触发属性更改支持对象,以向已注册支持对象的任何侦听器触发通知消息,如 @ 987654322@ 方法。

通过这种方式,控制器可以在模型上为它想要监听的任何属性注册监听器,然后通知视图任何更改。例如:

class SpeedListener implements PropertyChangeListener {
    @Override
    public void propertyChange(PropertyChangeEvent evt) {
        float speed = model.getSpeed();
        view.setSpeed(speed);
    }
}

设置可能类似于:

import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.GridLayout;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;

import javax.swing.*;
import javax.swing.event.SwingPropertyChangeSupport;

public class MVC2 {

    private static void createAndShowGui() {
        MvcModel model = new MvcModel();
        MvcView view = new MvcView();
        MvcController controller = new MvcController(model, view);
        controller.init();

        JFrame frame = new JFrame("MVC2");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.getContentPane().add(view.getMainDisplay());
        frame.pack();
        frame.setLocationRelativeTo(null);
        frame.setVisible(true);
    }

    public static void main(String[] args) {
        SwingUtilities.invokeLater(() -> createAndShowGui());
    }
}

class MvcView {
    private JPanel mainPanel = new JPanel();
    private JSlider speedSlider = new JSlider(0, MvcModel.MAX_SPEED);
    private JSlider batterySlider = new JSlider(0, 100);
    private JProgressBar speedBar = new JProgressBar(0, MvcModel.MAX_SPEED);
    private JProgressBar batteryPercentBar = new JProgressBar(0, 100);

    public MvcView() {
        speedSlider.setMajorTickSpacing(5);
        speedSlider.setMinorTickSpacing(1);
        speedSlider.setPaintTicks(true);
        speedSlider.setPaintLabels(true);
        speedSlider.setPaintTrack(true);

        batterySlider.setMajorTickSpacing(20);
        batterySlider.setMinorTickSpacing(5);
        batterySlider.setPaintTicks(true);
        batterySlider.setPaintLabels(true);
        batterySlider.setPaintTrack(true);

        speedBar.setStringPainted(true);
        batteryPercentBar.setStringPainted(true);

        JPanel inputPanel = new JPanel(new GridLayout(0, 1));
        inputPanel.add(createTitledPanel("Speed", speedSlider));
        inputPanel.add(createTitledPanel("Battery %", batterySlider));

        JPanel displayPanel = new JPanel(new GridLayout(0, 1));
        displayPanel.add(createTitledPanel("Speed", speedBar));
        displayPanel.add(createTitledPanel("Battery %", batteryPercentBar));

        mainPanel.setLayout(new GridLayout(1, 0));
        mainPanel.add(createTitledPanel("Input", inputPanel));
        mainPanel.add(createTitledPanel("Display", displayPanel));
    }

    private JComponent createTitledPanel(String title, JComponent component) {
        JPanel titledPanel = new JPanel(new BorderLayout());
        titledPanel.setBorder(BorderFactory.createTitledBorder(title));
        titledPanel.add(component);
        return titledPanel;
    }


    public JComponent getMainDisplay() {
        return mainPanel;
    }


    public void setSpeed(float speed) {
        speedBar.setValue((int) speed);
    }


    public void setBatteryPercent(int batteryPercent) {
        batteryPercentBar.setValue(batteryPercent);
    }


    public JSlider getSpeedSlider() {
        return speedSlider;
    }

    public JSlider getBatterySlider() {
        return batterySlider;
    }

}

class MvcController {
    private MvcModel model;
    private MvcView view;

    public MvcController(MvcModel model, MvcView view) {
        this.model = model;
        this.view = view;

        model.addPropertyChangeListener(MvcModel.BATTERY, new BatteryListener());
        model.addPropertyChangeListener(MvcModel.SPEED, new SpeedListener());

        view.getSpeedSlider().addChangeListener(chngEvent -> {
            int value = view.getSpeedSlider().getValue();
            model.setSpeed(value);
        });

        view.getBatterySlider().addChangeListener(chngEvent -> {
            int value = view.getBatterySlider().getValue();
            model.setBatteryPercentage(value);
        });
    }

    public void init() {
        view.getSpeedSlider().setValue(10);
        view.getBatterySlider().setValue(100);

        model.setSpeed(10);
        model.setBatteryPercentage(100);
    }

    class SpeedListener implements PropertyChangeListener {
        @Override
        public void propertyChange(PropertyChangeEvent evt) {
            float speed = model.getSpeed();
            view.setSpeed(speed);
        }
    }

    class BatteryListener implements PropertyChangeListener {
        @Override
        public void propertyChange(PropertyChangeEvent evt) {
            int batteryPercent = model.getBatteryPercentage();
            view.setBatteryPercent(batteryPercent);
        }
    }    
}

旁注:Observer 和 Observable 在最新版本的 Java 中已被弃用,因此应该避免使用它们。

【讨论】:

  • 嗨!这实际上与我后来发现的非常接近。我将详细查看您的答案,对其进行测试,并尽快回复您。我也会相应地更新帖子。赞成 ;-) 谢谢!
  • 您好!我接受了这个答案。只是一个问题,虽然我了解 SwingPropertyChangeSupport 和 PropertyChangeSupport 之间的区别,但为什么或何时应该使用第一个?
  • @Fred:SwingPropertyChangeSupport 保证会在 Event Dispatch 线程上触发通知,并且只在这个线程上,因此应该用于 Swing GUI 通知。请注意,所有 Swing 组件都已将这些对象之一作为类的字段。
  • 非常感谢!祝你复活节快乐(如果你庆祝它)!
【解决方案2】:

在您的 update 方法实现中,您可以使用第一个参数 o 确定哪个 Observable 已更改,以及使用第二个参数 arg 在您调用时确定哪个值更改:notifyObservers(this.speed);

请注意,notifyObservers 的签名接受Object,而float 原语不是Object 的子类。

【讨论】:

  • 嗨@Sal。在我的实现中,我只有一个 Observable,因此不需要第一个参数。但是关于第二个参数,我没有完全理解,所以我知道 float 不是 Object 的子类,但在这种情况下它没有用,因为它实际上并没有告诉你哪个字段/值已经改变,而是 Class/Instance (?) 如果适用。一种可能的解决方案是拥有一个具有属性 String 的对象,该属性在每个 notifyObservers 调用之前定义,并将该对象传递给观察者,观察者读取字符串并识别字段。但它似乎是硬编码的。
  • 只需使用相等运算符==,它会比较对象引用。 if(this.model.speed == arg)
  • 好主意!我将对此进行调查并尽快为您更新。谢谢。
  • 再次嗨,鉴于观察者模式在 Java 9 中已被弃用,尽管我感谢您的帮助,但我将进一步调查来自 @Hovercraft Full of Eels 的答案。你有我的赞成票! :-)
猜你喜欢
  • 2012-07-09
  • 2010-09-23
  • 2012-09-02
  • 1970-01-01
  • 2014-05-15
  • 2011-03-25
  • 2015-02-12
  • 2013-03-11
  • 2016-02-20
相关资源
最近更新 更多