[英]How to make this Strategy-Object pattern type safe
对于 tl;dr 版本,请参见此处: 链接
我很抱歉这堵文字墙,但请耐心等待。 我在这个问题上付出了很多努力,我相信手头的问题对这里的许多人来说应该很有趣。
我正在编写一个带有经典场景图的 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 的泛型类型参数在这里丢失了,我还没有找到以有意义的方式引入它们的方法。 这意味着 Actions 可以非法添加到没有定义它们的组件中。 看看这个驱动程序类的一个例子,现在无法在编译时检测到的错误类型:
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
);
}
}
我做了一些尝试来处理/改善这种情况:
我愿意接受任何建议,即使是那些需要对架构进行大修的建议。 唯一的要求是,不会丢失任何功能,并且使用该框架仍然足够简单,而且声明不会因泛型而负担过重。
以下是可用于编译和测试代码的 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;}
}
}
我很抱歉,但尽我所能,除了您的registerAction()
方法的Action
参数可能采用通配符而不是非泛型之外,我无法弄清楚您的代码有任何问题。 通配符是可以的,因为由于Action
类的类型参数中已经提出的限制,没有人可以定义类似Action<String,Object>
东西。
看看下面的变化。 我刚刚添加了一个static Map<>
来保存注册的项目并在registerAction()
添加到它。 我不明白为什么这种方法应该是一个问题。
public static abstract class Component{
/* Just as a sample of the registry of actions. */
private static final Map<Object, Action<?,?>> REGD = new HashMap<>();
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
REGD.put( key, action );
}
/* Just to test. */
public static Map<Object, Action<?, ?>> getRegd(){ return REGD; }
// attributes, constructors & methods omitted for brevity.
}
以下是我为实现这一目标而进行的更改。 请告诉我这是否适合您。
1. 将Component
类更改为此。 我已经解释了它的代码之后的变化。
public static abstract class Component<T extends Component<?>>{
Class<? extends T> type;
Component( Class<? extends T> type ){
this.type = type;
}
private Map<Object, Action<?,?>> REGD = new HashMap<>();
public void registerAction(Object key, Action<? super T,?> action) {
// the action is mapped to the key (for reference) and
// "somehow" connected to the internal event propagation system
REGD.put( key, action );
}
public Map<Object, Action<?, ?>> getRegd(){ return REGD; }
// attributes, constructors & methods omitted for brevity.
}
请注意以下更改:
Component
实例代表哪种类型。Class
实例的构造函数,使子类型在创建时声明其确切类型。registerAction()
接受Action<? super T>
Action<? super T>
而已。 也就是说,对于ListView
,接受ListView
或Collection
上的操作,但不接受Button
。 2.所以, Button
类现在看起来像这样:
public static class Button extends Component<Button>{
Button(){
super( Button.class );
}
public static final Object PRESS_SPACE_KEY = "";
public static final Action<Button, ?> PRESS_SPACE_ACTION = Action.FOR (Button.class)
.WHEN (MouseClick.class)
.DO ((col, evt) -> System.out.println("List has been clicked at: " + evt.getPoint()));
}
3. Collection
是另一个为扩展而设计的类,它声明了一个类似的构造函数, ListView
实现了它。
public static abstract class Collection<T extends Collection> extends Component<T>{
Collection( Class<T> type ){
super( type );
}
public static final Object CLICK_ITEM_KEY = "CLICK_ITEM_KEY";
/**
* A strategy that enables items within this Collection to be selected upon mouse click.
*/
public static final Action<Collection, Event.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 (Event.MouseClick.class)
// this condition must be true when the event happens
.IF ((collection, mouseClickEvent) ->
true //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.
}
public static class ListView extends Collection<ListView> {
ListView(){
super( ListView.class );
// TODO Auto-generated constructor stub
}
public static final Object ARROW_UP_KEY = "ARROW_UP_KEY";
/**
* 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, Event.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 (Event.KeyPress.class)
// this condition must be true when the event happens
.IF ((list, keyPressEvent) -> true
/*keyPressEvent.getKey() == Event.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.
}
4.相应地, Action
类声明更改为class Action<C extends Component<?>, E extends Event>
。
整个代码作为另一个类的内部类,以便于在您的 IDE 上进行分析。
public class Erasure2{
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
);
personList.getRegd().forEach( (k,v) -> System.out.println( k + ": " + v ) );
}
public static abstract class Component<T extends Component<?>>{
Class<? extends T> type;
Component( Class<? extends T> type ){
this.type = type;
}
private Map<Object, Action<?,?>> REGD = new HashMap<>();
public void registerAction(Object key, Action<? super T,?> action) {
// the action is mapped to the key (for reference) and
// "somehow" connected to the internal event propagation system
REGD.put( key, action );
}
public Map<Object, Action<?, ?>> getRegd(){ return REGD; }
// attributes, constructors & methods omitted for brevity.
}
public static class Button extends Component<Button>{
Button(){
super( Button.class );
}
public static final Object PRESS_SPACE_KEY = "";
public static final Action<Button, ?> PRESS_SPACE_ACTION = Action.FOR (Button.class)
.WHEN (MouseClick.class)
.DO ((col, evt) -> System.out.println("List has been clicked at: " + evt.getPoint()));
}
public static abstract class Collection<T extends Collection> extends Component<T>{
Collection( Class<T> type ){
super( type );
}
public static final Object CLICK_ITEM_KEY = "CLICK_ITEM_KEY";
/**
* A strategy that enables items within this Collection to be selected upon mouse click.
*/
public static final Action<Collection, Event.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 (Event.MouseClick.class)
// this condition must be true when the event happens
.IF ((collection, mouseClickEvent) ->
true //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.
}
public static class ListView extends Collection<ListView> {
ListView(){
super( ListView.class );
// TODO Auto-generated constructor stub
}
public static final Object ARROW_UP_KEY = "ARROW_UP_KEY";
/**
* 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, Event.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 (Event.KeyPress.class)
// this condition must be true when the event happens
.IF ((list, keyPressEvent) -> true
/*keyPressEvent.getKey() == Event.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.
}
public static 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 static 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;}
}
}
}
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.