在绘图应用程序中分离模型和视图/控制器

Posted

技术标签:

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

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

一些背景:

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

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

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

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

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

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

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

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

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

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

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

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


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

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

【问题讨论】:

【参考方案1】:

基本注意事项

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

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

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

初步建议

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

【讨论】:

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

ControlPointFigure 实现具有意义,并且具有当前的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 接口扩展作为内部类。

我假设下一个用户流:

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

【讨论】:

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

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

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

【讨论】:

感谢您的回答。我知道 Graphics2D api。但问题不仅仅是如何在 java 中绘制形状,而是如何在不硬连接模型和视图/控制器层的情况下处理复杂的形状操作(如上图所示)。

以上是关于在绘图应用程序中分离模型和视图/控制器的主要内容,如果未能解决你的问题,请参考以下文章

在 Rails 中分离后端和前端服务器

模态视图控制器从自定义单元格选择问题中分离 - iOS 7

浅谈 MVC 和 MVVM 模型

模型视图控制器与边界控制实体

如何从数组列表中分离数据并将其显示到列表视图中?

使用 Objective C 在 iOS8 中分离横向和纵向视图