不可变事件处理

5
我在Java中实现了一个不可变系统。几乎每个类都是不可变的,它的效果比我预期的要好得多。
我的问题在于尝试发送事件。通常你会有一个事件源和一个事件监听器。源简单地持有对监听器的引用,并在事件发生时发送它。
但是使用不可变对象时,当你修改一个字段并创建一个新对象时,事件监听器引用会发生变化。因此,事件源将发送到一些已被垃圾回收的旧引用。
因此,出于这个原因,所有我的GUI类都是可变的,因为它们自然地使用了很多事件。但是我希望找到一种优雅的方式来处理事件,以便我也可以使它们成为不可变的。
编辑:如下是所需的示例代码。
public final class ImmutableButton {
    public final String text;

    public ImmutableButton(String text) {
        this.text = text;
    }

    protected void onClick() {
        // notify listeners somehow, hoping they haven't changed
    }
}

public final class ImmutableWindow {
    public final ImmutableButton button;

    public ImmutableWindow(ImmutableButton button) {
        this.button = button;
    }

    protected void listenForButtonClick() {
        // somehow register with button and receive events, despite this object
        // being entirely recreated whenever a field changes
    }
}

嗯...我不太清楚“但是使用不可变对象时,当您修改字段并创建新对象时,事件监听器引用会更改”这部分的含义。据我理解,引用永远不会改变。而且一个对象将永远不会处理已被GC收集的引用。 - Little Santi
1
拥有不可变对象并对其进行更改反应的事件是没有意义的。要么它们是不可变的,不能被更改,因此对它们进行更改事件是没有意义的;要么在您更改的对象上有更改事件,但这意味着它们不再是不可变的。请在您的问题中添加源代码和/或一些图表,向我们展示您想要做什么。 - Progman
谁在谈论“change”事件?你为什么认为每个事件都需要对数据更改作出反应?如果你看到我实际使用的示例,那是一个GUI按钮按下后不会对按钮本身造成任何改变的情况。 - LegendLength
我真的不明白为什么你需要这个的源代码。这是一个关于不可变对象和事件处理的一般性问题。你在这方面有任何经验吗? - LegendLength
1
@LegendLength 通常不会出现不可变对象和/或事件的问题。因此,我认为你正在处理某些问题,或者认为你的设计存在一些问题。当你向我们展示一些源代码或一些图表,说明为什么你会得到一个旧引用作为事件源,我们可以帮助你解决问题。 - Progman
显示剩余8条评论
3个回答

3
GUI界面是一个很好的例子,其中可变性更加方便且性能更高。您可以创建一个支持读写操作的字段,用于GUI组件的一个备份模型。用户对显示进行更改,这些更改会反映在备份模型上 - 所有这些都发生在一个对象中。想象一下,每次用户更改一个组件时都必须重新创建一个对象?这肯定会拖慢您的系统。
尽管存在缺点,如果您真的希望GUI对象不可变,您可以创建一个全局事件总线来解决侦听器附加问题。通过这种方式,您无需担心事件侦听器将注册的对象实例。事件总线将负责分发事件,注册侦听器以及维护它们之间的映射关系。
以下是一个简单事件总线的草图设计。
public class EventBus {

    private Map<Event, List<EventListener>> REGISTRY;

    public void registerEventListener(Event event, EventListener listener) {
        List<EventListener> listeners = REGISTRY.getOrDefault(event, new ArrayList<>());
        listeners.add(listener);
    }

    public void fireEvent(Event event, Object... args) {
        List<EventListener> listeners = REGISTRY.get(event);

        if(listeners != null) {
            for(EventListener listener : listeners) {
                listener.handleEvent(args);
            }
        }
    }
}

// The events
enum Event {
    ADD_BUTTON_CLICKED, DELETE_BUTTON_CLICKED;
}

// Listeners must conform to one interface
interface EventListener {
    public void handleEvent(Object... args);
}

编辑

监听器是处理程序 - 它们应该执行业务逻辑而不保持状态。此外,它们不应该附加到组件上。在您上面的代码中,监听器代码必须与ImmutableWindow分离 - 两者都应该独立存在。 ImmutableWindowImmutableButton之间的交互必须在应用程序启动期间的某个地方(事件总线)进行配置。

您还应该拥有一个UI组件的中央注册表,在其中可以通过唯一ID识别它,并使用此注册表查找(遍历组件树)组件的最新实例并与之交互。

实际上,可能会像这样...

// The main class. Do the wirings here.
public class App {

    @Inject
    private EventBus eventBus;

    @PostConstruct
    public void init() {
        ImmutableWindow window = new ImmutableWindow ();
        ImmutableButton addButton = new ImmutableButton ();

        eventBus.registerEventListener(Events.ADD_BUTTON_CLICKED, new AddButtonClickListener());
    }
}

public class AddButtonClickListener implements EventListener  {

    @Inject
    private SomeOtherService someOtherSvc;

    @Inject
    private UiRegistry uiRegistry;

    public void handleEvent(Object... args) {
        ImmutableButton addButton = args[0].getSource; // The newset instance of the button must be packed and delivered to the eventlistners when firing an event
        ImmutableWindow targetWindow = uiRegistry.lookUp("identifier_of_the_window", ImmutableWindow.class);

        // Perform interaction between the two components;
    }
}

现在您拥有完全解耦的UI和业务逻辑。您可以随意重新创建组件,因为监听器不会受到影响,因为它们未附加到任何组件上。

事件总线似乎只是转移了问题。当监听器被重新创建(即更改状态)时,它需要重新注册到总线上。总线有助于集中所有内容,但在这方面仍然不太优雅。至于GUI不适合的问题,我同意,但我的问题是关于不可变系统和事件的一般性问题。GUI只是一个简单的例子,展示了整个问题的困难之处。 - LegendLength
问题在于UiRegistry.lookup()仍然需要查找对象列表以找到正确的对象。因此,每当重新创建对象时,都需要更新该列表。现在您不能依赖垃圾回收,因为您必须在每次对象删除和创建时更新该列表。但是,您肯定会获得奖励,因为除了您自己之外,没有人理解这个问题,而且这是我在过去几个月中发布的第二或第三篇文章。 - LegendLength

1
是的,你设计中的冲突在于不能对在重新构建不可变数据模型时被替换的对象注册监听器。我下面的解决方案是完全从模型中删除监听器。即使大多数对象都是不可变的,你仍需要至少一个可变变量作为基础来保存创建/重建的窗口。在这里,我使用内部类作为监听器,你只需注册一次。它们然后针对模型中的对象分派doStuff()调用,但是它们会根据单个基础可变窗口引用进行查找。例如,window.getButton1().doStuff(); 我不认为这是一个很好的解决方案,但它是我能为你的要求想出的最简单和最清晰的解决方案。监听器不会失效,从窗口到下面的所有内容都可以是不可变的。
public final MutableBase {
    private ImmutableWindow window;  // single mutable variable

    public MutableBase() {
        Magic.registerClickListener(new WindowClickEvent());
        Magic.registerClickListener(new Button1ClickEvent());
        rebuildWindow();
    }

    public void rebuildWindow() {
        // rebuild here when needed - change single mutable variable
        this.window = new ImmutableWindow(new ImmutableButton("text"));
    }

    class WindowClickEvent implements ClickListener {
        public void onClick() {
            this.window.doStuff();
        }
    }

    class Button1ClickEvent implements ClickListener {
        public void onClick() {
            this.window.getButton1().doStuff();
        }
    }
}

1
我认为不可能将所有内容都变成不可变的,因为你需要在某个地方存储一个变化的系统状态(通常是在模型中)。让我举个例子来解释一下。很抱歉回复有点长,但我尽量清晰明了,因为这是一个复杂的问题。
我假设你想要为你的GUI遵循MVC(模型-视图-控制器)模式。由于你的示例代码不够清晰,所以我会将其重写为适当的MVC代码。
在MVC中,视图V监听模型M的变化,并相应地更新显示。
通常情况下,像M这样的可监听对象包含一个集合(例如视图V和控制器C)。M必须保持在内存中的同一对象,因为它被多个用户(V和C)共享。它必须包含一个“addListener”函数,该函数更改原始对象中的集合。如果它只创建了M的修改本地副本,addListener将仅为调用addListener的对象添加侦听器,而不是原始对象,这些对象希望调用添加的侦听器。因此,通常情况下,可监听对象不能是不可变的。
让我更具体地说明一下,如果这太过密集。
你的代码不是适当的MVC,因此我将其修改为以下代码,引入了模型M。
public class Model {
  final?? boolean clicked = false;
  final?? Set<Listeners> listeners

  void addChangeListener(l) {
    listeners.add(l);
  }

  void doClick() { 
    clicked = true; // can not be final ? 
    listeners.forEach(l -> l.notifyChange(..event..));
  }

  boolean getClickedState() {
    return clicked; // makes no sense if final ? 
  }
}

public final class ImmutableWindow {
  final Model model;
  public ImmutableWindow(Model m) {
    this.model = m;
    this.model.addChangeListener( evt -> 
      updatePanel(this.model.getClickedState() );
  }
}

public final class ImmutableButton {
  final Model model;
  public ImmutableButton(Model m) {
    this.model = m
  }

    protected void onClick() {
        this.model.doClick();
    }
}

模型扩展了监听器模式。现在它甚至有两个属性需要可变: 1. 模型必须反映系统的实际状态。 2. 模型是可观察的,因此包含一个可变的监听器集合。
@m.doClick和m.getClickedState()只有在模型是可变的时候(改变模型/系统状态)才能工作。模型的clicked字段不能是final。
正如上面所讨论的那样,可听对象不能是不可变的。
现在注意到ImmutableButton和ImmutableWindow不再是真正的不可变了,因为模型将变得可变。尽管如此,“final”注释仍将起作用,因为final仅指对象引用,而不是所引用对象的内容。
或者,您可能建议使模型不可变,并让其转发模型的所有更改给其侦听器。但是,ImmutableButton将不得不存储模型的更新。但如果这样做,ImmutableButton.model就不能是final,而ImmutableButton对象将不再是不可变的...
最终,某人必须存储更改的对象。在MVC中,它在模型中,但无论您做什么,存储更改的对象都将是不可变的。
我认为上述事件总线的想法试图将模型隐藏在某些接口后面。这个想法是接口可以是相同的固定(甚至是静态)对象,它可以指向一些可变数据。但它不起作用,因为任何指向某些可变对象的对象也是可变的。具体来说,包含可变 eventBus 的任何对象都是可变的。
更彻底的做法可能是尝试删除整个事件和监听器。但是我不知道如何在没有共享监听器系统的情况下进行事件处理。
我最好的解决方案是最小化模型的可变性。只在模型中放置一个可变对象,例如SystemState,并仅对其进行变异。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接