【问题标题】:How to make this Strategy-Object pattern type safe如何使这种策略对象模式类型安全
【发布时间】:2020-02-02 17:07:48
【问题描述】:

这是我之前提出的问题的加长版

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

我很抱歉这堵文字墙,但请多多包涵。我在这个问题上付出了很多努力,我相信这里的许多人应该对手头的问题感兴趣。

背景

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

为了推广良好的编程原则,如单一职责、关注点分离等,组件的功能实现为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
        );
    }

}

我尝试了什么?

我做了一些尝试来处理/改善这种情况:

  1. 尝试覆盖 Component 的每个子类中的 registerAction 方法。由于泛型类型擦除在 java 中是如何实现的,这将不起作用。更多详情请参考my earlier question
  2. Component 的每个子类引入一个泛型类型参数,该参数始终与 Component 的类型相同。同样的解决方案已被建议为answer in my previous question。我不喜欢这种解决方案,因为所有声明都会变得过分夸大。我知道在实践中这将导致用户完全放弃类型安全,因为他们更喜欢可读性而不是类型安全。因此,虽然这在技术上是一种解决方案,但它不适用于我的用户。
  3. 忽略它。如果一切都失败了,这就是显而易见的 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


【解决方案1】:

很抱歉,但尽我所能,我无法找出您的代码有任何问题,除了您的 registerAction() 方法的 Action 参数可能采用通配符而不是非泛型。通配符是可以的,因为由于Action 类的类型参数中已经存在的限制,没有人可以定义像Action&lt;String,Object&gt; 这样的东西。

看看下面的变化。我刚刚添加了一个static Map&lt;&gt; 来保存已注册的项目并将其添加到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.

}

【讨论】:

  • 因为它允许您将按钮的操作添加到 ViewList,这将在运行时导致 ClassCastException。在强类型语言中不应该发生的事情。
【解决方案2】:

以下是我为实现这一目标所做的更改。请告诉我这是否适合您。

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.

}

注意以下变化:

  1. 引入了泛型类型以了解Component 实例代表的类型。
  2. 通过添加一个采用其类型的 Class 实例的构造函数,使子类型在创建时声明其确切类型。
  3. registerAction() 仅接受 Action&lt;? super T&gt;。也就是说,对于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&lt;C extends Component&lt;?&gt;, E extends Event&gt;



将整个代码作为另一个类的内部类,以便在您的 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;}
        }

    }
}

【讨论】:

  • 这个解决方案可以改进,我们不需要Component 的子类来声明构造函数。那就是把Component的构造函数改成Component(){ this.type = (Class&lt;T&gt;) this.getClass(); }。不过,我还没有真正深刻地考虑过这样做的后果。
猜你喜欢
  • 2015-04-12
  • 1970-01-01
  • 2018-01-07
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多