【问题标题】:Separating Model and View/Controller in a drawing application在绘图应用程序中分离模型和视图/控制器
【发布时间】:2015-07-21 06:10:24
【问题描述】:

我正在开发一个矢量绘图应用程序(在 java 中),我正在为我的模型类和视图/控制器类之间的分离而苦苦挣扎。

一些背景:

您可以绘制不同的形状:
矩形、线条和饼图段

有 4 种工具可用于操作画布上的形状:
缩放工具、移动工具、旋转工具和变形工具

对于这个问题,变形工具是最有趣的一个: 它允许您通过拖动其中一个点并调整其他属性来更改形状,如下图所示:

这些转换规则对于每个形状都是不同的,我认为它们是模型业务逻辑的一部分,但在某种程度上它们需要暴露给视图/控制器(工具类),以便它们可以应用正确的规则。

此外,形状通过不同的值在内部表示: - 矩形存储为中心、宽度、高度、旋转 - 线存储为起点和终点 - 饼图段存储为中心、半径、角度1、角度2

我计划在未来添加更多形状,例如星星、气泡或箭头,每个形状都有自己的控制点。

我还计划在未来添加更多工具,例如旋转或缩放形状组。

每个工具的控制点都不同。比如使用缩放工具时,不能抓取中心点,但是每个缩放控制点需要关联一个轴心点(或者多个让用户选择)。

对于矩形、直线和饼图等简单形状,类的每个实例的控制点都是相同的,但未来形状(如贝塞尔路径或星形)(具有可配置的尖峰计数)将具有不同数量的控制点实例。

那么问题是什么是建模和实现这些控制点的好方法?

由于它们对于每个工具都略有不同,并且带有一些工具/控制器特定的数据,因此它们以某种方式属于工具/控制器。但由于它们也特定于每种形状并带有非常重要的领域逻辑,它们也属于模型。

我想避免每次添加一个工具或形状时为每个工具/形状组合添加一种特殊类型的控制点的组合爆炸。


更新: 再举一个例子:将来我可能会想到一个我想要支持的新形状:圆弧。它类似于饼图段,但看起来有点不同,拖动控制点时的行为完全不同。

为了实现这一点,我希望能够创建一个实现我的 Shape 接口的 ArcShape 类并完成。

【问题讨论】:

    标签: java user-interface oop model-view-controller vector-graphics


    【解决方案1】:

    基本注意事项

    首先让我们为简单起见做一些定义。

    Entity 是一个领域模型对象,它定义了所有的结构和行为,即逻辑。 EntityUI 是 UI 中代表 Entity 的图形控件。

    所以基本上,对于Shape 类,我认为ShapeUI 必须非常了解Shape 的结构。该结构主要由我猜的控制点组成。换句话说,拥有所有关于控制点的信息(可能是未来的向量),ShapeUI 将能够在 UI 上绘制自己。

    初步建议

    我对@9​​87654330@ 类的建议是Shape 类定义了所有行为。 ShapeUI 类将知道 Shape 类并保留对它所代表的类的引用,通过它它可以访问控制点,并能够操纵它们,例如设置他们的位置。 Observer 模式只是要求在这种情况下使用。特别是Shape类可以实现Observable,ShapeUI会实现Observer并订阅对应的Shape对象。

    所以基本上在这种情况下会发生什么,ShapeUI 对象将处理所有 UI 操作,并将负责更新 Shape 参数,例如控制点位置。之后,一旦发生位置更新,Shape 对象就会在状态更改时执行其逻辑,然后盲目地(不知道ShapeUI)通知ShapeUI 更新状态。因此,ShapeUI 将相应地绘制新状态。在这里您将获得低耦合模型和视图。

    至于Tools,我个人的看法是每个Tool都必须知道如何操作每种类型的Shape,即每个形状的操作逻辑必须在Tool类中实现。对于视图和模型的解耦,它与Shape 几乎相同。 ToolUI 类处理点击光标的位置、点击了什么ShapeUI、点击了哪个控制点等。通过获取这些信息,ToolUI 将把它传递给适当的Tool 对象,然后它将根据接收到的参数应用逻辑。

    处理不同的形状类型

    现在,当谈到 Tool 以自己的方式处理不同的 Shapes 时,我认为 Abstract Factory 模式介入了。每个工具都将实现一个 Abstract Factory,我们将为每种类型提供操作实现Shape.

    总结

    根据我的建议,这里是域模型草案:

    为了从我的建议中了解整个想法,我还发布了特定用例的序列图:

    使用ToolUI,用户点击ShapeUI的ControlPointUI

    【讨论】:

      【解决方案2】:

      如果我理解正确,这里是我们所拥有的:

      • 不同的人物都有控制点
      • 用户界面允许绘制图形并拖动控制点

      我的建议是,图形的特征放在模型层,而 UI 部分放在视图/控制器层。

      模型更进一步:

      • figures 应该实现一个接口:

        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是一种抽象,可以表示线段、圆弧或贝塞尔曲线

      • 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 应该指示哪个工具可以使用控制点以及用于哪种用途 - 根据您的要求,rotate 工具应该将中心视为特殊的。

      • a Pos 代表 x,y 坐标

      这样,用户界面就不必知道这些数字实际上是什么。

      对于draw a Figure,UI 获取Segment 的列表并简单地独立绘制每个Segment,并在每个控制点添加一个标记。拖动控制点时,UI 会为 Figure 提供新位置并重新绘制它。它应该能够在将Figure 重绘到新位置之前将其擦除,或者(更简单但更慢)它可以在每次操作时全部重绘

      使用drag 方法,我们只能在单个形状上拖动一个简单的控制点。它很容易扩展,但必须为每个工具添加扩展。例如,我已经添加了rotate 方法,该方法允许通过移动一个具有定义中心的控制点来旋转形状。您还可以添加 scale 方法。

      多种形状

      如果您想对一组形状应用变换,您可以使用矩形的子类。您构建一个边平行于包含所有形状的坐标轴的矩形。我建议在Figure 中添加一个方法,该方法返回一个(相当小的)封闭矩形,其边平行于坐标轴,以简化多形状矩形的创建。然后,当您将变换应用到 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 接口扩展作为内部类。

        我假设下一个用户流:

        1. 已选择工具 - 控制器内部的 currentTool 设置为来自枚举的适当值。
        2. 用户选择/选择形状 - 调用getPoints,根据工具,某些类型的点可能会被过滤掉。例如。只为变形操作返回角点。为暴露点注入适当的策略。
        3. 当用户拖动点时 - 调用了 translate,您就可以使用给定工具转换新形状。

        【讨论】:

        • 什么是翻译策略?一个点需要存储对它的引用似乎很奇怪,它增加了不必要的状态。我不喜欢 Shape(model) 需要知道 ToolsEnum,但我看到在图形应用程序中,您可以以某种方式将工具计数到模型层。无论如何我都会给你 +1,因为这似乎是一种直截了当的方式,即使它不像我想要的那样干净。
        • TranslationStrategy 是具有单一方法Shape translate(int x, int y) 的接口。更严格地说,在策略模式中,您将拥有 Point 将扩展的单独接口。如果你愿意,你可以用TranslationStrategyFactory 代替枚举。您能否提供代码来说明您将如何使用 100% 清洁解决方案?
        • 如果我对每种类型的点(角点、原点...)都有不同的类或接口,如果它们本身就是一种策略,为什么每个点都需要一个额外的 Strategy 对象?跨度>
        • 我无法为您提供 100% 干净的代码,因为我没有。我有一个我已经实现的工作应用程序,在某种程度上它就像你和 Serge Ballesta 建议的混合体。但最后我感觉它的实现方式并不正确,出于学习目的,我想从头开始。所以我在这里寻找其他人如何解决这个问题。
        • 相同类型的点可以根据所选工具执行不同的操作。例如。角点可以缩放或变形。此外,当您引入新工具时,您只需要实施其他策略。 OCP就在那里。但是,如果您可以添加更多点类型(例如,在缩放路径之外)并将所有工具映射到这些类型上。这也很好。
        【解决方案4】:

        原则上,让模型与绘图界面相匹配是个好主意。因此,例如,在 Java Swing 中,可以使用drawRect 方法绘制矩形,该方法将左上角的x、y、宽度和高度作为参数。因此,通常您希望将矩形建模为{ x-UL, y-UL, width, height }。

        对于任意路径,包括圆弧,Swing 为GeneralPath 对象提供了一些方法,用于处理通过直线或二次/贝塞尔曲线连接的一系列点。要为 GeneralPath 建模,您可以提供点列表、缠绕规则以及二次曲线或贝塞尔曲线的必要参数。

        【讨论】:

        • 感谢您的回答。我知道 Graphics2D api。但问题不仅仅是如何在 java 中绘制形状,而是如何在不硬连接模型和视图/控制器层的情况下处理复杂的形状操作(如上图所示)。
        猜你喜欢
        • 2017-07-05
        • 2011-08-11
        • 1970-01-01
        • 1970-01-01
        • 2011-03-06
        • 1970-01-01
        • 2017-12-30
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多