Шаблон проектирования для замены повторяющегося кода при изменении свойств одного объекта

У меня есть 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-интерфейса модели, чтобы включить эту операцию в качестве службы высокого уровня, которую он предлагает.