如何使此策略对象模式类型安全

问题描述 投票:0回答:2

这是我先前问过的问题的长版

对于tl; dr版本,请参见此处:Link

我为这堵文字墙感到抱歉,但请多多包涵。我在这个问题上花了很多力气,我认为这里的问题应该让许多人感兴趣。

背景

我正在用经典的Scene Graph编写UI框架。我有一个名为Component的抽象顶级类和许多子类,其中一些是具体的,而另一些也抽象。具体的子类可能是Button,而抽象的子类可能是Collection。中级类Collection是诸如ListViewTreeViewTableView之类的超类,并且包含所有这些子类共享的通用功能。

为了促进良好的编程原则,例如单一职责,关注点分离等,组件的功能实现为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,可以通过按键盘上的相应箭头键将选择项“上移”和“下移”。此功能也可以通过策略模式作为操作来实现:

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. } 您可以看到,动作的[[通用类型参数]在这里丢失了,我还没有找到一种以有意义的方式介绍它们的方法。这意味着可以将操作非法添加到未定义操作的组件中。请看一下此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类的代码和Event的代码,可用于编译和测试代码:

    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;} } }

  • java generics strategy-pattern type-safety
    2个回答
    0
    投票
    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. }

    0
    投票
    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>。也就是说,对于ListView,接受ListViewCollection上的动作,但不接受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;} } } }
  • © www.soinside.com 2019 - 2024. All rights reserved.