How to make this Strategy-Object pattern type safe

南楼画角 提交于 2020-03-04 19:42:47

问题


This is the long version of a question I asked earlier

For the tl;dr version please see here: Link

I am sorry for this wall of text, but please bear with me. I put a lot of effort into the question and I believe the problem at hand should interesting to many here.

Background

I am writing a UI Framework with a classic Scene Graph. I have an abstract Top-Level class called Component and many subclasses, some of which are concrete while others abstract as well. A concrete subclass might be Button while an abstract subclass is Collection. The Mid-Level class Collection is the supertype for such classes as ListView, TreeView or TableView and contains common functionality that all of these subclasses share.

To promote good programming principles such as Single Responsibility, Separation of Concerns, etc, features of Components are implemented as Strategy-Objects. These can be added to and removed from components at runtime to manipulate their behaviour. See the example below:

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.

}

The example is obviously heavily simplified but hopefully the meaning can be understood without seeing the implementations of the many methods used within.

Many instances of the Action are defined throughout the framework in the same way as above. This way the behaviour of each Component can be precisely controlled by developers using the framework.

A subclass of Collection is ListView which extends Collection by mapping integer indices to items in the collection. For a ListView it is possible to move the selection "up" and "down" by pressing the corresponding arrow keys on the keyboard. This feature is also implemented via the strategy pattern as an 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.

}

Problem

These features so far work as intended. The problem arises with how these actions are registered at a component. My current idea was to have a method registerAction in the Component class:

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.

}

As you can see, the generic type parameters of action are lost here and I have not found a way to introduce them in a meaningful way. This means that Actions can illegaly be added to components for which they are not defined. Take a look at this driver class for an example of the kind of error that can not be detected at compile-time right now:

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

}

What have I tried?

I made a few attempts to deal with / improve the situation:

  1. Try to overwrite the registerAction method in each subclass of Component. This will not work because of how generic type erasure is implemented in java. For more details refer to my earlier question.
  2. Introduce a generic type parameter to each subclass of Component which will always be identical to the type of Component. This same solution has been suggested as an answer in my previous question. I don't like this solution because all declarations will become hugely overblown. I know that in practice this will lead to users just abandoning the type safety entirely because they prefer readability over type safety. So although this is technically a solution it will not work for my users.
  3. Just ignore it. This is the obvious Plan B if all else fails. In this case runtime type checking is all that can be done.

I am open to any suggestions, even those that require a major overhaul of the architecture. The only requirements are, that no functionality is lost and working with the framework is still simple enough without declarations becoming overburdened with generics.

Edit

Here is the Code for the Action class and code for the Events that can be used to compile and test the code:

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

}

回答1:


I am sorry, but try as I might, I could not figure out any issue with your code except that your registerAction() method's Action parameter may take wildcard instead of being non-generic. Wildcard is OK, since no one can define something like Action<String,Object> anyhow due to the restrictions already posed in the type parameters of Action class.

Have a look at the change below. I have just added a static Map<> to hold the registered items and adding to it in registerAction(). I don't see why this method should be a problem at all.

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.

}



回答2:


Here are the changes I brought in to make this happen. Please tell me if this works for you.

1. Change Component class to this. I have explained the changes after its code.

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.

}

Note the following changes:

  1. Introduced a generic type to know which type a Component instance represents.
  2. Made the subtypes declare their exact type on creation by adding a constructor that takes the Class instance of their type.
  3. registerAction() accepts Action<? super T> only. That is, for ListView, an action that is on ListView or Collection is accepted but not of Button.

2. So, Button class now looks like this:

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. And the Collection being another class designed for extension, declares a similar constructor, which ListView implements.

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. Correspondingly, the Action class declaration changes to class Action<C extends Component<?>, E extends Event>.



The entire code as inner classes of another class, for easier analysis on your 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;}
        }

    }
}


来源:https://stackoverflow.com/questions/60028518/how-to-make-this-strategy-object-pattern-type-safe

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!