在绘图应用程序中分离模型和视图/控制器
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 上绘制自己。
初步建议
我对@987654330@ 类的建议是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
以自己的方式处理不同的 Shape
s 时,我认为 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 工具应该将中心视为特殊的。
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
方法绘制矩形,该方法将左上角的x
、y
、宽度和高度作为参数。因此,通常您希望将矩形建模为 x-UL, y-UL, width, height
。
对于任意路径,包括圆弧,Swing 为GeneralPath 对象提供了一些方法,用于处理通过直线或二次/贝塞尔曲线连接的一系列点。要为 GeneralPath 建模,您可以提供点列表、缠绕规则以及二次曲线或贝塞尔曲线的必要参数。
【讨论】:
感谢您的回答。我知道 Graphics2D api。但问题不仅仅是如何在 java 中绘制形状,而是如何在不硬连接模型和视图/控制器层的情况下处理复杂的形状操作(如上图所示)。以上是关于在绘图应用程序中分离模型和视图/控制器的主要内容,如果未能解决你的问题,请参考以下文章