Это длинная версия вопроса, который я задавал ранее
Для версии 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
);
}
}
Что я пробовал?
Я предпринял несколько попыток справиться с ситуацией или улучшить ее:
- Попробуйте переписать метод registerAction в каждом подклассе Component . Это не будет работать из-за того, как стирание типа c реализовано в java. Подробнее см. мой предыдущий вопрос .
- . Введите параметр типа generi c для каждого подкласса Component , который всегда будет идентичен типу компонента. , Это же решение было предложено в качестве ответа в моем предыдущем вопросе . Мне не нравится это решение, потому что все объявления будут сильно раздуты. Я знаю, что на практике это приведет к тому, что пользователи просто полностью откажутся от безопасности типов, потому что они предпочитают удобочитаемость, а не безопасность типов. Поэтому, хотя это технически решение, оно не будет работать для моих пользователей.
- Просто игнорируйте его. Это очевидный План Б, если ничего не помогает. В этом случае проверка типов во время выполнения - это все, что можно сделать.
Я открыт для любых предложений, даже тех, которые требуют серьезного пересмотра архитектуры. Единственное требование состоит в том, чтобы ни одна функциональность не терялась, а работа с фреймворком все еще достаточно проста, и объявления не перегружены обобщениями.
Редактировать
Вот код класса 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;}
}
}