【问题标题】:The Elegant way to handle Cyclic Event in Java?在 Java 中处理循环事件的优雅方式?
【发布时间】:2011-01-24 20:13:11
【问题描述】:

我认为这对我来说不是一个特定的问题;每个人之前可能都遇到过这个问题。 为了正确地说明它,这里有一个简单的 UI:

如您所见,这两个微调器控制着一个变量——“A”。唯一的区别是他们使用不同的视图来控制它。

由于这两个微调器的显示值是同步的,所以会出现循环事件。

如果我更改顶部微调器,“A”将被更改,底部微调器的值也将相应更新。 但是,更新底部微调器的调用(例如 setValue)也会触发另一个事件,指示顶部微调器根据底部微调器的值进行更新。从而创建了一个坏循环,最终可能导致 StackOverFlow 异常。

我之前的解决方案有点麻烦:我放置了一个保护布尔值来指示是否应该执行第二次更新调用。

现在我想问“我怎样才能优雅地处理这种情况?一般来说,不是特定于纺纱厂)”

谢谢


更新:

因为我有 2 个答案建议我使用观察者结构,所以我不得不说一下。

就像我所说的那样,它很棒,但远非完美。不仅因为它固有的复杂性,还因为它无法解决问题

为什么?要了解原因,您必须实现Java Swing 中View 和Model-Controller 的紧密耦合。让我们以我的微调器 UI 为例。假设变量 A 实际上是一个 Observer 对象。然后,在从顶部微调器触发第一个状态更改事件后,观察者“A”将更新其值并触发 PropertyChange 事件以通知底部微调器。然后是第二次更新,它更新了底部微调器的视图。 然而,改变底部微调器的视图不可避免地会触发一个冗余事件,该事件将尝试再次设置“A”的值。之后,致命循环完全构建,堆栈溢出将被抛出。

理论上,观察者模型试图通过引入2条独立的反馈路径来解决直接循环。链式更新几率(在事件响应代码中)隐式形成连接两条路径的桥梁,再次形成循环。

【问题讨论】:

    标签: java swing events awt cyclic


    【解决方案1】:

    回到模型-视图-控制器,想想你的模型是什么,你的视图是什么。

    在您当前的实现中,您有两个模型(每个 Spinner 控件一个),它们通过视图层进行同步。

    您应该做的是共享相同的支持模型。对于减去值的微调器,创建原始模型的代理。即:

    class ProxySpinnerModel implements SpinnerModel {
        getValue() { return originalSpinner.getValue() - 10 }
        setValue(v) { originalSpinner.setValue(v+10) }
    }
    
    spinnerA = new JSpinner()
    spinnerB = new JSpinner( new ProxySpinnerModel( spinnerA.getModel() ) )
    

    现在,您不需要添加侦听器,因为它们都在使用同一个模型,并且默认实现(originalModel)已经具有向视图触发的更改侦听器。

    【讨论】:

    • 太棒了!我在自己的回复中做了一个图表,以便与其他建议进行比较。欢迎提出建议。
    【解决方案2】:

    问题已解决


    我有很多不同的建议。特别, 我要感谢 Marc W 和 Reverend Gonzo。我是来对这些想法做一个总结的;这可以节省您浏览大量文本的时间。

    如果你仔细地解耦视图和模型控制器,这个问题可以很容易地绕过。 死循环是由依赖写入引起的:write_1 -> write_2 -> write_1 ->...。直觉上,打破依赖可以优雅地解决问题。

    如果我们深入研究这个问题,我们会发现更新相应的视图并不一定涉及外部写入调用。实际上,视图只取决于它所代表的数据。知道了这一点,我们可以重写逻辑如下:write_1 -> read_2 & write_2 -> read_1

    为了说明这个想法,让我们比较不同海报提到的 3 种方法: alt text http://www.freeimagehosting.net/uploads/2707f1b483.png

    如您所见,只有代理视图才能解决所有依赖关系,因此它是解决此类问题的通用解决方案。

    实际上,它可以这样实现(在您的事件响应代码中):

     setValue(newValue);
     anotherSyncUI.parse();  // not anotherSyncUI.setValue() any more
     anotherSyncUI.repaint();
    

    没有更多的循环。解决了。​​

    【讨论】:

    • 我不太确定这是否被清楚地描述,但我很高兴您找到了您理解的解决方案!我希望您能够理解此处海报所提出的所有概念。
    • 谢谢马克。我相信一个恰当的例子是人们理解的最佳方式。 :)
    • 如果您需要调用重绘,则有问题(缺少模型/代理之一的通知)
    【解决方案3】:

    这有点复杂,但你可以让A 实际上是一个可观察的对象。两个微调器(或任何需要根据A 的值更新自身的东西)都会观察到A。每当A 更改时,微调器(或再次,任何对象)都会更新自己以反映A 的新值。这将微调器的逻辑彼此分离。在您的示例中,微调器不应相互耦合,因为它们彼此之间确实没有任何关系。相反,它们都应该简单地绑定到 A 并单独处理自己的视图更新。

    每当第一个微调器中的值发生更改时,您只需更新A 的值以匹配它。每当第二个微调器中的值发生更改时,您当然会在其值上加 10,然后再将其分配给 A

    更新

    针对您对原始问题的更新,我的回答是微调器不会相互监听彼此的更改事件。每个微调器都有一个单独的事件处理方法。用户单击微调器中的向上或向下箭头会生成与以编程方式在微调器上调用 setValue 不同的事件,对吗?如果微调器完全相互独立,则不会出现无限循环。

    【讨论】:

    • 这是一个具有巨大潜力和架构优雅的解决方案。它类似于文档和文本组件链接在一起的方式。一个主要的DRAWBACK 是为每个变量类型创建一个观察者会使一个简单的问题变得非常复杂。因此,不可能完全替换保护布尔值。我给了你一个肯定的投票,但我现在不能将它标记为正确的答案(可能稍后)。让我们看看来自不同人的才华横溢的大脑的更多建议。 :)
    • 我在答案中添加了更新以反映您的问题的更新。
    • 那还是有问题的。这个问题不涉及我是部署一个还是两个事件响应方法。原因在于,这两个微调器是相互依赖的,因为它们的视图是同步的。
    • 将更改监听器放在鼠标点击/按键上是一种非常复杂的(Swing 最佳实践不正确)捕获更改事件和同步控件的方法。
    • dex,正如其他人在这里所说的那样,您不应该认为微调器是依赖的​​。仅仅因为它们显示来自同一来源的数据并不会使它们相互依赖。它们都单独依赖于相同的数据源,仅此而已。如果你能这样想,问题就会变得容易解决。
    【解决方案4】:

    例如对于第二个微调器,计算 A-10,然后将其与微调器的当前值进行比较。如果相同,则什么都不做,结束无限循环。第一个微调器也是如此。

    我认为还有一些方法可以在不触发事件的情况下更新微调器的模型,但我不知道它们是什么。

    【讨论】:

    • 我曾经使用比较的方法。不久,我发现它不像布尔保护方法那么简单。此外,使用微调器只是举例。它并不总是旋转器。有时,比较可能很困难或很麻烦。是的,最优雅的方法之一是阻止事件触发。
    【解决方案5】:

    为两个 JSpinner 使用一个 SpinnerModel。请参阅以下代码: 请注意,每次由其中一个 JSpinner 定义新值时,只调用一次 setValue()。

    import java.awt.BorderLayout;
    
    import javax.swing.*;
    
    public class Test {
        public static void main(String[] args) {
            JFrame jf = new JFrame();
            SpinnerModel spinModel = new MySpinnerModel();
            JSpinner jspin1 = new JSpinner(spinModel);
            JSpinner jspin2 = new JSpinner(spinModel);
            jf.setLayout(new BorderLayout());
            jf.add(jspin1, BorderLayout.NORTH);
            jf.add(jspin2, BorderLayout.SOUTH);
            jf.pack();
            jf.setVisible(true);
            jf.setDefaultCloseOperation(3);
        }
    }
    
    class MySpinnerModel extends AbstractSpinnerModel {
        private int _value = 0;
        private int _min = 0;
        private int _max = 10;
    
        @Override
        public Object getNextValue() {
            if (_value == _max) {
                return null;
            }
            return _value + 1;
        }
    
        @Override
        public Object getPreviousValue() {
            if (_value == _min) {
                return null;
            }
            return _value - 1;
        }
    
        @Override
        public Object getValue() {
            return _value;
        }
    
        @Override
        public void setValue(Object value) {
            System.out.println("setValue(" + value + ")");
            if (value instanceof Integer) {
                _value = (Integer) value;
                fireStateChanged();
            }
        }
    }
    

    【讨论】:

    • 这是一种智能同步方法。但是,这不是我要寻找的答案。请仔细阅读我的问题。我更关心这类问题的通用解决方案。
    【解决方案6】:

    看来你真的观察错了。从给出的示例中,我假设您想要检测的是用户对控件的操作,而不是值本身的变化。正如您所概述的,您的模型中的变化反映在微调器的值中,正是这形成了事件的无限循环。

    但是,深入研究 UI 实现可能不是您想要的答案。在那种情况下,我会说你能做的最好的事情就是你当前的后卫解决方案,或者更好地将逻辑提取到你的模型中(类似于 Marc 和 William 所说的)。如何做到这一点将取决于所提供拼图的特定实现背后的“真实世界”模型。

    【讨论】:

    • 是的。 +1 票。但是,我相信这个问题有一个通用的解决方案。只是我们还没有发现。
    • 我在更新的答案中也提到了这一点(在阅读你的答案之前,史蒂文)。不要观察 UI 控件;观察模型中的值(在本例中为A)。
    【解决方案7】:

    通常,您的模型不应由您的 GUI 定义。即,支持每个 JSpinner 的 SpinnerModel 不应该是您的值 A。(这将是对特定视图的非常不优雅的紧耦合依赖。)

    相反,您的值 A 应该是 POJO 或对象的属性。在这种情况下,您可以向其添加 PropertyChangeSupport。 (并且在任何情况下都可能已经这样做了,因为如果您的程序的其他部分更改了 A,您希望您的微调器自动更新)。

    我意识到这类似于 Marc W 的回答,您担心它“复杂”,但 PropertyChangeSupport 几乎为您完成了所有工作。

    事实上,对于非常简单的情况,您可以只使用一个类,将“setProperty”方法连接到“firePropertyChange”调用(以及将值存储在 HashMap 中以用于任何“getProperty”调用)。

    【讨论】:

      【解决方案8】:

      我并不想解决您的问题,但我觉得这很有趣。我已经面对它,并且每次都以不同的方式解决它。但是当我想到“为什么?”而不是关于“如何?”我一直很困惑。
      之所以存在这个问题,是因为我使用了一个必须帮助我的自动化(MVC),而且正是以这种方式。如何使用组件的艺术使这种自动化成为编写漂亮代码的障碍。
      为什么 set #setEvent() 必须产生与 GUI 操作相同的事件?

      【讨论】:

      • 一个有趣的想法。然而,我不觉得这是一个漂亮的代码的障碍,但缺乏功能。例如,如果事件对象参数可以提供更多信息以显示调用链,则可以和平解决此问题。
      【解决方案9】:

      虽然,我的看法也很接近观察者模式,但比那要轻一些!!!

      将 A 作为带有 setter 的变量

      private Integer A;
      
      setA(int A)
      {
        this.A = A;
        refreshSpinners();
      }
      
      
      refreshSpinners()
      {
        setSpinnerA();
        setSpinnerAMinus10();
      }
      
      setSpinnerA()
      {
         // show value of A
      }
      
      setSpinnerAMinus10()
      {
         // show value of A-10
      }
      

      【讨论】:

      • 嗨,谢谢,但它仍然有问题。请记住,这些微调器不仅用作视图,还用作控制器。如果用户单击一个微调器的按钮,则会发生堆栈溢出。 有关详细信息,请阅读我的更新。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2023-01-21
      • 2015-04-10
      • 2018-12-02
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多