【发布时间】:2020-02-02 17:07:48
【问题描述】:
这是我之前提出的问题的加长版
对于 tl;dr 版本,请参见此处:Link
我很抱歉这堵文字墙,但请多多包涵。我在这个问题上付出了很多努力,我相信这里的许多人应该对手头的问题感兴趣。
背景
我正在编写一个带有经典 Scene Graph 的 UI 框架。我有一个名为 Component 的抽象顶级类和许多子类,其中一些是具体的,而另一些也是抽象的。具体子类可能是Button,而抽象子类可能是Collection。中级类 Collection 是 ListView、TreeView 或 TableView 等类的超类型,并包含通用功能所有这些子类共享。
为了推广良好的编程原则,如单一职责、关注点分离等,组件的功能实现为Strategy-Objects。这些可以在运行时添加到组件中或从组件中删除以操纵它们的行为。请看下面的例子:
public abstract class Collection extends Component {
/**
* A strategy that enables items within this Collection to be selected upon mouse click.
*/
public static final Action<Collection, MouseClick> CLICK_ITEM_ACTION =
// this action can only be added to components for which Collection.class.isInstance(component) == true
Action.FOR (Collection.class)
// this action will only happen when a MouseClick event is delivered to the component
.WHEN (MouseClick.class)
// this condition must be true when the event happens
.IF ((collection, mouseClickEvent) ->
collection.isEnabled() && collection.hasItemAt(mouseClickEvent.getPoint())
)
// these effects will happen as a reaction
.DO ((collection, mouseClickEvent) ->
collection.setSelectedItem(collection.getItemAt(mouseClickEvent.getPoint()))
)
;
// attributes, constructors & methods omitted for brevity.
}
这个例子显然被大大简化了,但希望不用看到其中使用的许多方法的实现就能理解其含义。
Action 的许多实例以与上述相同的方式在整个框架中定义。这样,每个组件的行为都可以由使用框架的开发人员精确控制。
Collection 的子类是 ListView,它通过将整数索引映射到集合中的项目来扩展 Collection。对于 ListView,可以通过按下键盘上相应的箭头键来“向上”和“向下”移动选择。此功能也通过策略模式作为 Action 实现:
public class ListView extends Collection {
/**
* A strategy that enables the selection to be moved "up" (that is to an item with a lower index)
* upon pressing the UP arrow key.
*/
static final Action<ListView, KeyPress> ARROW_UP_ACTION =
// this action can only be added to components for which ListView.class.isInstance(component) == true
Action.FOR (ListView.class)
// this action will only happen when a KeyPress event is delivered to the component
.WHEN (KeyPress.class)
// this condition must be true when the event happens
.IF ((list, keyPressEvent) ->
keyPressEvent.getKey() == ARROW_UP && list.isEnabled()
&& list.hasSelection() && list.getSelectedIndex() > 0
)
// these effects will happen as a reaction
.DO ((list, keyPressEvent) ->
list.setSelectedIndex(list.getSelectedIndex() - 1)
)
;
// attributes, constructors & methods omitted for brevity.
}
问题
到目前为止,这些功能按预期工作。问题在于如何在组件中注册这些操作。我目前的想法是在 Component 类中有一个方法 registerAction:
public abstract class Component {
public void registerAction(Object key, Action action) {
// the action is mapped to the key (for reference) and
// "somehow" connected to the internal event propagation system
}
// attributes, constructors & methods omitted for brevity.
}
如您所见,action 的泛型类型参数在这里丢失了,我还没有找到一种有意义的方式来介绍它们。这意味着动作可以非法添加到未定义它们的组件中。看看这个driver class,看看现在在编译时无法检测到的那种错误的例子:
public class Driver {
public static void main(String[] args) {
ListView personList = new ListView();
// this is intended to be possible and is!
personList.registerAction(
Collection.CLICK_ITEM_KEY,
Collection.CLICK_ITEM_ACTION
);
personList.registerAction(
ListView.ARROW_UP_KEY,
ListView.ARROW_UP_ACTION
);
// this is intended to be possible and is!
personList.registerAction(
"MyCustomAction",
Action.FOR (Collection.class)
.WHEN (MouseClick.class)
.DO ((col, evt) -> System.out.println("List has been clicked at: " + evt.getPoint()))
);
// this will eventually result in a runtime ClassCastException
// but should ideally be detected at compile-time
personList.registerAction(
Button.PRESS_SPACE_KEY,
Button.PRESS_SPACE_ACTION
);
}
}
我尝试了什么?
我做了一些尝试来处理/改善这种情况:
- 尝试覆盖 Component 的每个子类中的 registerAction 方法。由于泛型类型擦除在 java 中是如何实现的,这将不起作用。更多详情请参考my earlier question。
- 为 Component 的每个子类引入一个泛型类型参数,该参数始终与 Component 的类型相同。同样的解决方案已被建议为answer in my previous question。我不喜欢这种解决方案,因为所有声明都会变得过分夸大。我知道在实践中这将导致用户完全放弃类型安全,因为他们更喜欢可读性而不是类型安全。因此,虽然这在技术上是一种解决方案,但它不适用于我的用户。
- 忽略它。如果一切都失败了,这就是显而易见的 B 计划。在这种情况下,可以进行运行时类型检查。
我愿意接受任何建议,即使是那些需要对架构进行大修的建议。唯一的要求是,不会丢失任何功能,并且使用框架仍然足够简单,而且声明不会因泛型而负担过重。
编辑
下面是 Action 类的代码和可用于编译和测试代码的事件代码:
import java.util.function.BiConsumer;
import java.util.function.BiPredicate;
public class Action<C extends Component, E extends Event> {
private final Class<E> eventType;
private final BiPredicate<C, E> condition;
private final BiConsumer<C, E> effect;
public Action(Class<E> eventType, BiPredicate<C, E> condition, BiConsumer<C, E> effect) {
this.eventType = eventType;
this.condition = condition;
this.effect = effect;
}
public void onEvent(C component, Event event) {
if (eventType.isInstance(event)) {
E evt = (E) event;
if (condition == null || condition.test(component, evt)) {
effect.accept(component, evt);
}
}
}
private static final Impl impl = new Impl();
public static <C extends Component> DefineEvent<C> FOR(Class<C> componentType) {
impl.eventType = null;
impl.condition = null;
return impl;
}
private static class Impl implements DefineEvent, DefineCondition, DefineEffect {
private Class eventType;
private BiPredicate condition;
public DefineCondition WHEN(Class eventType) {
this.eventType = eventType;
return this;
}
public DefineEffect IF(BiPredicate condition) {
this.condition = condition;
return this;
}
public Action DO(BiConsumer effect) {
return new Action(eventType, condition, effect);
}
}
public static interface DefineEvent<C extends Component> {
<E extends Event> DefineCondition<C, E> WHEN(Class<E> eventType);
}
public static interface DefineCondition<C extends Component, E extends Event> {
DefineEffect<C, E> IF(BiPredicate<C, E> condition);
Action<C, E> DO(BiConsumer<C, E> effects);
}
public static interface DefineEffect<C extends Component, E extends Event> {
Action<C, E> DO(BiConsumer<C, E> effect);
}
}
public class Event {
public static final Key ARROW_UP = new Key();
public static final Key SPACE = new Key();
public static class Point {}
public static class Key {}
public static class MouseClick extends Event {
public Point getPoint() {return null;}
}
public static class KeyPress extends Event {
public Key getKey() {return null;}
}
public static class KeyRelease extends Event {
public Key getKey() {return null;}
}
}
【问题讨论】:
-
您能否也为
Action类添加一个最小代码?然后我们可以试试代码。 -
完成;见编辑。请注意,出于本主题的目的,这些也被大量缩写。
-
动作是在全局注册表中注册的吗,只有一个,还是每个
Component子类型实例都有自己的注册Actions的映射? -
这些组件保留了一个本地的动作映射,但根据具体情况,这些动作也会在“全局”事件传播系统中注册。它很复杂,但细节对于这里使用的策略模式应该不重要。
标签: java generics strategy-pattern type-safety