Шаблон проектирования для замены повторяющегося кода при изменении свойств одного объекта
У меня есть 4 пользовательских прядильщика, которые изменяют ширину, высоту, местоположение X и местоположение Y одного выбранного виджета. Мой виджет можно перетаскивать по экрану, и идея состоит в том, чтобы использовать эти прядильщики для изменения определенных свойств, таких как ширина или высота, и увидеть немедленное влияние на изменения.
Есть ли шаблон, который я могу использовать для замены всех этих классов (XSpinnerListener, YSpinnerListener...) только с одним и указать, какое свойство моего текущего объекта (JButton) необходимо изменить? Это хороший дизайн?
public void init(){
widthSpinner.setListener(new WidthSpinnerListener());
heightSpinner.setListener(new HeightSpinnerListener());
xSpinner.setListener(new XSpinnerListener());
ySpinner.setListener(new YSpinnerListener());
}
public class XSpinnerListener implements SpinnerListener {
@Override
public void spinnerValueChanged() {
current.setLocation(xSpinner.getValue(), current.getY());
}
}
public class YSpinnerListener implements SpinnerListener {
@Override
public void spinnerValueChanged() {
current.setLocation(current.getX(), ySpinner.getValue());
}
}
public class WidthSpinnerListener implements SpinnerListener {
@Override
public void spinnerValueChanged() {
current.setSize(widthSpinner.getValue(), current.getHeight());
}
}
public class HeightSpinnerListener implements SpinnerListener {
@Override
public void spinnerValueChanged() {
current.setSize(current.getWidth(), heightSpinner.getValue());
}
}
Ответы
Ответ 1
Некоторые размышления...
Вы можете эмулировать дизайн Swing, предоставив вашему SpinnerListener spinnerValueChanged(...)
метод параметр SpinnerEvent, который указывает, какая ось изменяется. Ось может быть инкапсулирована перечислением,...
enum Axis {
X("X"), Y("Y"), WIDTH("Width"), HEIGHT("Height");
private String name;
private Axis(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
И класс параметров SpinnerEvent может выглядеть примерно так:
class SpinnerEvent {
private Object source;
private Axis axis;
private int value;
public SpinnerEvent(Object source, Axis axis, int value) {
this.source = source;
this.axis = axis;
this.value = value;
}
public Object getSource() {
return source;
}
public Axis getAxis() {
return axis;
}
public int getValue() {
return value;
}
}
Ваш интерфейс SpinnerListener (который вы нам не показываете) изменил бы:
interface SpinnerListener {
public void SpinnerValueChanged(SpinnerEvent e);
}
И, возможно, ваши конкретные реализации могут работать на объектах, реализующих движимый интерфейс:
interface Movable {
public abstract int getX();
public abstract void setX(int x);
public abstract int getY();
public abstract void setY(int y);
public abstract int getWidth();
public abstract void setWidth(int width);
public abstract int getHeight();
public abstract void setHeight(int height);
public abstract void move(Axis axis, int value);
}
с помощью ключевого метода, перемещение, которое можно реализовать следующим образом:
@Override
public void move(Axis axis, int value) {
switch (axis) {
case X:
x += value;
break;
case Y:
y += value;
break;
case WIDTH:
width += value;
break;
case HEIGHT:
height += value;
default:
break;
}
}
Малая реализация бетона
class ConcreteSpinnerListener implements SpinnerListener {
private Movable movable;
public ConcreteSpinnerListener(Movable movable) {
this.movable = movable;
}
@Override
public void SpinnerValueChanged(SpinnerEvent e) {
movable.move(e.getAxis(), e.getValue());
}
}
Ответ 2
В подобных ситуациях я всегда выполняю все операции всякий раз, когда что-то меняется, если нет штрафов за производительность:
public void init(){
SpinnerListener spinnerListener = new MySpinnerListener();
widthSpinner.setListener(spinnerListener);
heightSpinner.setListener(spinnerListener);
xSpinner.setListener(spinnerListener);
ySpinner.setListener(spinnerListener);
}
public class MySpinnerListener implements SpinnerListener {
@Override
public void spinnerValueChanged() {
updateLocationAndSize();
}
}
void updateLocationAndSize() {
current.setLocation(xSpinner.getValue(), ySpinner.getValue());
current.setSize(widthSpinner.getValue(), heightSpinner.getValue());
}
Таким образом, код короче и намерение становится понятным для других читателей.
Кроме того, еще одно преимущество этого подхода состоит в том, что проще синхронизировать начальные триггеры действия (прядильщики) с потребителями действий (компонент current
).
Это может быть или не быть применимым или полезным в вашем примере, но предположим, что вы открываете экран с сохраненными атрибутами размера и местоположения для current
. Вы инициализируете прядильщики с сохраненными значениями, затем вы вызываете updateLocationAndSize
для синхронизации размера и местоположения current
с прядильщиками, а затем регистрируете слушателя, чтобы получать уведомления об инкрементальных изменениях.
Ответ 3
Принимая Java 8 и принимая на данный момент, что ваши прядильщики действительно имеют метод setListener()
, я бы просто реализовал SpinnerListener
как один конкретный класс, который использует преимущества lambdas. Заметив, что каждый из ваших SpinnerListener
вызывает метод в потоке, который принимает два аргумента int
, можно было бы представить что-то подобное в SpinnerListener:
BiConsumer<Integer, Integer> currentSetter;
Supplier<Integer> firstArg;
Supplier<Integer> secondArg;
public void spinnerValueChanged() {
currentSetter.accept(firstArg.get(), secondArg.get());
}
Его можно вызвать в init следующим образом:
public void init() {
widthSpinner.setListener(
new SpinnerListener(
{ a, b -> current.setSize(a, b) },
{ widthSpinner.getValue() },
{ current.getHeight() }));
// ... etc.
}
Ответ 4
С точки зрения дизайна ваша основная проблема, похоже, заключается в изоляции представления от элементов управления. Думаю, вы ищете модельный контроллер.
Модель - это то, что называется current
в OP.
Если настройка X-позиции является основной моделью, в API текущего должен быть setXLocation(int x)
.
Если это так, все четыре действия - это один лайнер, который связывает слайдер с соответствующим действием, используя интерфейс в качестве способа передачи указателей функций Java <= 1.7, так что вы в порядке, как есть. Вероятно, я сделал бы анонимных слушателей (сохранив фактический код, близкий к определению Slider, если он не используется повторно), но это просто стилистика.
В принципе, мы смешаем представление (сам слайдер) и элемент управления (код слушателя) немного, но в большинстве случаев это нормально (я бы пошел с этим в вашем примере и придерживался вашего текущего решения). Читаемость по-прежнему довольно хороша, хотя она многословна. Это просто Java, он будет работать отлично и будет поддерживаться на мой взгляд (насколько это любой GUI).
Если у вас есть много таких свойств для редактирования, или API модели уже хорош и не нуждается в этих тонких элементах управления, вы можете захотеть пойти с явным контроллером, то есть классом, который интерпретирует мелкие эффекты зерна setXLocation(x)
и соответствующим образом делегирует их фактическому текущему объекту setLocation(x,current.getLocY())
.
В ответном сообщении на воздушной подушке предлагается полная схема в этом направлении: сделайте ваши явные команды (на DP), создайте элементы представления, создайте команды и контроллер, чтобы их интерпретировать. Вам нужно довольно тяжелый вариант использования, чтобы спуститься по этому пути, хотя в конечном итоге он более надежный и расширяемый. И GUI действительно не имеют проблем с производительностью, если вы не делаете ничего глупого. Поэтому Драган отвечает также и на меня прагматичным и разумным. В некотором смысле он уже частично изолирует управляющий код от представления в фактическом методе setLocationAndSize.
Подводя итог, если код, который вы вводите в свой слушатель, является одним лайнером, вы отлично от проектной точки зрения.
Если код слушателя является сложным, он не должен быть "скрыт" в Listener: это вызывает фрагментацию кода, много дублирования в разных слушателях, важный код скрыт в графическом интерфейсе, когда это никогда не будет так.
Для более чем одного лайнера, заставить слушателя делегировать действительное действие более сложному контроллеру или рассмотреть возможность расширения API-интерфейса модели, чтобы включить эту операцию в качестве службы высокого уровня, которую он предлагает.