Подтвердить что ты не робот

Разделение модели и представления/контроллера в приложении рисования

Я работаю над приложением векторного рисования (в java), и я борюсь с разделением между моими классами моделей и классами view/controller.

Немного фона:

Вы можете рисовать разные формы:
прямоугольники, линии и сегменты пирога

Есть четыре инструмента для управления фигурами на холсте:
инструмент масштабирования, инструмент перемещения, инструмент поворота и инструмент морфинга

Для этого вопроса инструмент морфинга является наиболее интересным: Он позволяет изменять форму, перетаскивая одну из ее точек и настраивая другие свойства, как показано на этом рисунке:

The different options of manipulating a shape

Эти правила преобразования различны для каждой фигуры, и я думаю, что они являются частью бизнес-логики модели, но таким образом, что они должны быть подвержены представлению/контроллеру (классы инструментов), чтобы они могли применить правильный.

Кроме того, фигуры представлены внутри разных значений:  - Прямоугольник хранится как центр, ширина, высота, вращение  - Строка сохраняется как начальная и конечная точки  - сегмент пирога сохраняется как центр, радиус, угол1, угол2

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

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

Контрольные точки для каждого инструмента различны. Например, при использовании инструмента масштабирования вы не можете захватить центральную точку, но каждая контрольная точка масштабирования должна быть связана с одной точкой поворота (или несколькими, чтобы позволить пользователю выбрать).

Для простых фигур, таких как прямоугольник, линия и пирог, контрольные точки одинаковы для каждого экземпляра класса, но фьючерсы, такие как путь безье или звезда (с настраиваемым количеством спайков), будут иметь различное количество контрольных точек за экземпляр.

Итак, вопрос в том, что хороший способ моделирования и реализации этих контрольных точек?

Поскольку они немного различаются для каждого инструмента и несут некоторые данные конкретного инструмента/контроллера, они каким-то образом относятся к инструменту/контроллеру. Но поскольку они также специфичны для каждого типа формы и несут очень важную логику домена, они также относятся к модели.

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


Обновление:. Чтобы привести еще один пример: в будущем может возникнуть мысль, что у меня есть идея для новой формы, которую я хочу поддерживать: дуги. Он похож на сегмент пирога, но выглядит немного по-другому и ведет себя совершенно по-другому при перетаскивании контрольных точек.

Чтобы реализовать это, я хотел бы просто создать класс ArcShape, реализующий интерфейс Shape и сделанный.

The behaviour of the arc shape

4b9b3361

Ответ 1

Основные соображения

Прежде всего сделаем некоторые определения для простоты.

Entity - это объект модели домена, который определяет всю структуру и поведение, то есть логику. EntityUI - это графический элемент управления, который представляет Entity в пользовательском интерфейсе.

В принципе, для классов Shape я думаю, что ShapeUI должен быть в значительной степени осведомлен о структуре Shape. Структура в основном состоит из контрольных точек, которые я предполагаю. Другими словами, имея всю информацию о контрольных точках (возможно, векторы в будущем), ShapeUI сможет нарисовать себя в интерфейсе пользователя.

Первоначальные предложения

Что я предлагаю для классов Shape, так это то, что класс Shape определяет все поведение. Класс ShapeUI будет знать класс Shape и содержать ссылку на тот, который он представляет, посредством которого он будет иметь доступ к контрольным точкам, а также иметь возможность манипулировать ими, например. установить их местоположения. Шаблон Observer просто просит использовать в этом контексте. В частности, класс Shape может реализовать Observable, а ShapeUI будет реализовывать Observer и подписаться на соответствующий объект Shape.

Итак, в основном, что произойдет в этом случае, объект ShapeUI будет обрабатывать все операции пользовательского интерфейса и будет отвечать за обновление параметров Shape, например. местоположения контрольных точек. После этого, как только произойдет обновление местоположения, объект Shape выполняет свою логику при изменении состояния, а затем слепо (не зная ShapeUI) уведомляет ShapeUI об обновленном состоянии. Соответственно, ShapeUI будет рисовать новое состояние. Здесь вы получите модель с низкой связью и просмотр.

Что касается Tools, мое собственное мнение состоит в том, что каждый Tool должен знать, как манипулировать каждым типом Shape, то есть логика манипуляции каждой формы должна быть реализована внутри класса Tool. Для развязки вида и модели это почти то же самое, что и для Shape. Класс ToolUI обрабатывает щелчок курсором, на что он нажал ShapeUI, на какую контрольную точку он нажал и т.д. Получив эту информацию, ToolUI передаст ее соответствующему объекту Tool который затем применит логику на основе полученных параметров.

Обработка различных типов форм

Теперь, когда дело касается Tool обработки разных Shape по-своему, я думаю, что шаблон Abstract Factory работает. Каждый инструмент будет реализовывать Abstract Factory, где мы предоставим реализации манипуляций для каждого типа Shape.

Резюме

Основываясь на том, что я предложил, вот проект модели домена:

Domain Model

Чтобы получить всю идею из моих предложений, я также размещаю диаграмму последовательности для конкретного примера использования:

Используя ToolUI, пользователь нажимает ShapeUI ControlPointUI

enter image description here

Ответ 2

Если я правильно понимаю, вот что мы имеем:

  • разные цифры, все из которых имеют контрольные точки.
  • пользовательский интерфейс позволяет рисовать фигуры и перетаскивать контрольные точки.

Мой совет должен сказать, что то, что характеризует фигуру, находится в слое модели, а часть пользовательского интерфейса - в представлении/контроллере.

Еще один шаг для модели:

  • цифры должны реализовывать интерфейс:

    public interface Figure {
        List<Segment> segments();
        List<ControlPoint> controlPoints();
        void drag(ControlPoint point, Pos newPos);
        void rotate(ControlPoint point, Pos newPos, Pos center); // or rotate(Pos center, double angle);
    }
    
  • Segment - абстракция, которая может представлять собой отрезок линии, дугу или кривую Безье

  • a ControlPoint имеет смысл для реализации Figure и имеет текущий Pos

    public interface ControlPoint{
        Figure parent();
        void drag(Pos newPos); // unsure if it must exist in both interfaces
        Pos position();
        ToolHint toolHint();
    }
    
  • ToolHint должен быть признаком того, какой инструмент может использовать контрольную точку и для какого использования - по вашему требованию, инструмент поворота должен рассматривать центр как особый.

  • a Pos представляет координаты x, y

Таким образом, пользовательский интерфейс не должен знать ничего о том, что фигуры на самом деле.

В draw a Figure пользовательский интерфейс получает список Segment и просто проводит независимо каждый сегмент и добавляет отметку в каждой контрольной точке. Когда контрольная точка перетаскивается, пользовательский интерфейс дает новое положение Figure и перерисовывает его. Он должен иметь возможность стереть Figure перед тем, как перерисовать его в новом положении или, альтернативно (проще и медленнее), он может перерисовать все при каждой операции.

С помощью метода drag мы можем просто перетащить простую контрольную точку на одну фигуру. Он легко расширяется, но для каждого инструмента необходимо добавить расширения. Например, я уже добавил метод rotate, который позволяет поворачивать фигуру, перемещая одну контрольную точку с помощью центра определения. Вы также можете добавить метод масштабирования.

Несколько фигур

Если вы хотите применить преобразование к набору фигур, вы можете использовать подкласс прямоугольника. Вы создаете прямоугольник со сторонами, параллельными осям координат, которые содержат все фигуры. Я рекомендую добавить метод в Figure, который возвращает (разумно малый) охватывающий прямоугольник со сторонами parralel для координации осей, чтобы облегчить создание прямоугольника с несколькими формами. Затем, когда вы применяете преобразование к прямоугольнику englobing, он просто сообщает о преобразовании ко всем его элементам. Но мы приходим сюда к преобразованиям, которые не могут быть выполнены перетаскиванием контрольных точек, потому что перетаскиваемая точка не относится к внутренней форме.

Внутренние преобразования

До сих пор я занимался только интерфейсом между UI и моделью. Но с несколькими формами мы увидели, что нам нужно применять произвольные аффинные преобразования (перевод точки прямоугольника, зависящего от ангела, или масштабирование прямоугольника с ангелом) или поворот. Если мы решили реализовать поворот как rotate(center, angle), то поворот включенной фигуры уже сделан. Поэтому нам просто нужно реализовать аффинное преобразование

class AffineTransform {
    private double a, b, c, d;
    /* creators, getters, setters omitted, but we probably need to implement
       one creator by use case */

    Pos transform(Pos pos) {
         Pos newpos;
         newpos.x = a * pos.x + b;
         newpos.y = c * pos.y + d;
         return newpos;
    }
}

Таким образом, чтобы применить аффинное преобразование к Figure, нам просто нужно реализовать transform(AffineTransform txform) таким образом, чтобы просто применить все точки, определяющие структуру.

Рисунок:

    public interface Figure {
        List<Segment> segments();
        List<ControlPoint> controlPoints();
        void drag(ControlPoint point, Pos newPos);
        void rotate(Pos center, double angle);
        // void rotate(ControlPoint point, double angle); if ControlPoint does not implement Pos
        Figure getEnclosingRectangle();
        void transform(AffineTransform txform);
    }

Сводка:

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

Ответ 3

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

inteface Shape {
   List<Point> getPoints(ToolsEnum strategy); // you could use factory here
}

interface Point {
    Shape rotate(int degrees); // or double radians if you like
    Shape translate(int x, int y);
    void setStrategy(TranslationStrategy strategy);
}

interface Origin extends Point {}

interface SidePoint extends Point {}

interface CornerPoint extends Point {}

Затем реализуйте расширения Point расширения интерфейса как внутренние классы в каждой конкретной форме.

Я предполагаю следующий поток пользователей:

  • Выбранный инструмент - currentTool внутренний контроллер установлен в соответствующее значение из перечисления.
  • Пользователь выбирает/сжимает форму - getPoints, в зависимости от инструмента может быть отфильтрован некоторый тип точек. Например. возвращаются только угловые точки для операций морфинга. Внедрить соответствующие стратегии для открытых точек.
  • Когда пользователь перетаскивает точку - translate, и у вас есть новая форма, преобразованная с помощью данного инструмента.

Ответ 4

В принципе, это хорошая идея, чтобы модель соответствовала интерфейсу рисования. Так, например, в Java Swing прямоугольники могут быть нарисованы с помощью метода drawRect, который принимает в качестве аргументов x, y верхнего левого угла, ширины и высоты. Итак, обычно вы хотели бы моделировать прямоугольник как { x-UL, y-UL, width, height }.

Для произвольных путей, включая дуги, Swing предоставляет объект GeneralPath с методами для работы с последовательностью точек, связанных либо линиями или Квадратичные/Безье. Для моделирования GeneralPath вы можете предоставить список точек, правило обмотки и необходимые параметры либо квадратичной кривой, либо кривой Безье.