Как сделать этот тип шаблона Strategy-Object безопасным - PullRequest
0 голосов
/ 02 февраля 2020

Это длинная версия вопроса, который я задавал ранее

Для версии tl; dr смотрите здесь: Ссылка

Я прошу прощения за эту стену текст, но, пожалуйста, потерпите меня. Я вложил много усилий в этот вопрос и считаю, что эта проблема должна быть интересна многим здесь.

Справочная информация

Я пишу UI Framework с классом c График сцены . У меня есть абстрактный класс верхнего уровня с именем Component и много подклассов, некоторые из которых являются конкретными, а другие также абстрактными. Конкретный подкласс может быть Button , а абстрактный подкласс - Collection . Класс среднего уровня Collection является супертипом для таких классов, как ListView , TreeView или TableView и содержит общие функции, которые все эти общий доступ к подклассам.

Для продвижения хороших принципов программирования, таких как единая ответственность, разделение интересов и т. д. c, функции компонентов реализованы в виде объектов-стратегий . Они могут быть добавлены и удалены из компонентов во время выполнения, чтобы управлять их поведением. См. Пример ниже:

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.

}

Пример, очевидно, сильно упрощен, но, надеюсь, смысл можно понять, не видя реализации многих методов, используемых внутри.

Множество примеров Действие определяется во всей структуре так же, как и выше. Таким образом, поведение каждого компонента может точно контролироваться разработчиками, использующими каркас.

Подкласс 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.

}

Проблема

Эти функции до сих пор работают, как и предполагалось . Проблема возникает с тем, как эти действия регистрируются в компоненте. Моя текущая идея состояла в том, чтобы иметь метод registerAction в классе Component :

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.

}

Как видите, параметры типа generi c действий здесь потеряны, и я не нашел способа представить их осмысленно. Это означает, что Действия могут быть незаконно добавлены к компонентам, для которых они не определены. Взгляните на этот класс драйверов для примера ошибки, которая не может быть обнаружена прямо сейчас во время компиляции:

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. Попробуйте переписать метод registerAction в каждом подклассе Component . Это не будет работать из-за того, как стирание типа c реализовано в java. Подробнее см. мой предыдущий вопрос .
  2. . Введите параметр типа generi c для каждого подкласса Component , который всегда будет идентичен типу компонента. , Это же решение было предложено в качестве ответа в моем предыдущем вопросе . Мне не нравится это решение, потому что все объявления будут сильно раздуты. Я знаю, что на практике это приведет к тому, что пользователи просто полностью откажутся от безопасности типов, потому что они предпочитают удобочитаемость, а не безопасность типов. Поэтому, хотя это технически решение, оно не будет работать для моих пользователей.
  3. Просто игнорируйте его. Это очевидный План Б, если ничего не помогает. В этом случае проверка типов во время выполнения - это все, что можно сделать.

Я открыт для любых предложений, даже тех, которые требуют серьезного пересмотра архитектуры. Единственное требование состоит в том, чтобы ни одна функциональность не терялась, а работа с фреймворком все еще достаточно проста, и объявления не перегружены обобщениями.

Редактировать

Вот код класса 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;}
    }

}

Ответы [ 2 ]

0 голосов
/ 02 февраля 2020

Вот изменения, которые я внес, чтобы это произошло. Скажите, пожалуйста, работает ли это для вас.

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. Введен обобщенный тип c, чтобы узнать, какой тип представляет экземпляр Component.
  2. Сделано, чтобы подтипы объявляли свой точный тип при создании, добавляя конструктор, который принимает экземпляр Class их типа.
  3. registerAction() принимает только 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;}
        }

    }
}
0 голосов
/ 02 февраля 2020

Извините, но, как бы я ни пытался, я не смог выяснить какую-либо проблему с вашим кодом, за исключением того, что параметр Action вашего метода registerAction() может принимать подстановочный знак вместо того, чтобы не быть универсальным c. Подстановочный знак в порядке, так как никто не может определить что-то вроде Action<String,Object> в любом случае из-за ограничений, уже установленных в параметрах типа класса Action.

Посмотрите на изменения ниже. Я только что добавил 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.

}
...