如何使此策略对象模式类型安全 [英] How to make this Strategy-Object pattern type safe

查看:79
本文介绍了如何使此策略对象模式类型安全的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

对于tl版本,请参见此处:链接

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.

我正在用经典的场景图编写UI框架.我有一个名为 Component 的抽象顶级类,还有许多子类,其中一些是具体的,而另一些也是抽象的.一个具体的子类可能是 Button ,而一个抽象的子类可能是 Collection .中级类 Collection 是诸如 ListView TreeView TableView 之类的类的超类型,并且包含通用功能所有这些子类都共享.

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.

Collection 的子类是 ListView ,它通过将整数索引映射到集合中的项来扩展 Collection .对于 ListView ,可以通过按下键盘上的相应箭头键来将选择内容上移"和下移".此功能还可以通过策略模式作为操作来实现:

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.

}

问题

到目前为止,这些功能可以正常工作.问题是如何在组件上注册这些操作.我目前的想法是在 Component 类中具有方法 registerAction :

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. 尝试覆盖 Component 的每个子类中的 registerAction 方法.由于在Java中如何实现泛型类型擦除,因此这将无法正常工作.有关更多详细信息,请参见我之前的问题.
  2. >
  3. Component 的每个子类引入通用类型参数,该参数始终与Component的类型相同.建议在我上一个问题中作为答案使用相同的解决方案.我不喜欢这种解决方案,因为所有声明都会变得过分夸大.我知道实际上,这将导致用户完全放弃类型安全性,因为他们更喜欢可读性而不是类型安全性.因此,尽管从技术上讲这是一种解决方案,但它对我的用户不起作用.
  4. 只需忽略它.如果其他所有方法都失败,这就是显而易见的计划B.在这种情况下,只需执行运行时类型检查即可.
  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.

这是Action类的代码和Event的代码,可用于编译和测试代码:

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

}

推荐答案

以下是我为实现此目的而进行的更改.请告诉我这是否适合您.

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

1..将Component类更改为此.我已经解释了代码后的更改.

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.

}

请注意以下更改:

  1. 引入了一种通用类型,以了解Component实例代表哪种类型.
  2. 通过添加采用其类型的Class实例的构造函数,使子类型在创建时声明其确切类型.
  3. registerAction()仅接受Action<? super T>.也就是说,对于ListView,接受ListViewCollection上的操作,但不接受Button.
  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.这样,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.并且Collection是另一个为扩展设计的类,声明了一个类似的构造函数,由ListView实现.

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..相应地,Action类声明更改为class Action<C extends Component<?>, E extends Event>.

4. Correspondingly, the Action class declaration changes to class Action<C extends Component<?>, E extends Event>.

将整个代码作为另一个类的内部类,以便在您的IDE上进行更轻松的分析.

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

    }
}

这篇关于如何使此策略对象模式类型安全的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆