17

这完全是一个最佳实践类型的问题,因此语言无关紧要。我了解 MVC 的基本原理,并且它有不同的微妙风格(即直接引用模型的视图与来自控制器的数据委托)。

我的问题是关于跨 MVC 通信,当这些 MVC 嵌套时。这方面的一个例子是绘图程序(如 Paint 或其他东西)。Canvas 本身可以是 MVC,但每个绘制的实体(例如 Shapes、Text)也可以。从模型的角度来看,拥有一个实体集合是有意义的CanvasModel,但是 和 应该CanvasView分别CanvasController拥有相应的实体视图和控制器集合吗?

另外,添加新绘制实体的最佳/最干净的方法是什么?假设用户激活了 CircleTool,他们在 Canvas 视图中单击并开始绘制形状。CanvasView可以触发CanvasController可以侦听的相关鼠标向下/移动/向上事件。然后控制器基本上可以将这些事件代理到 CircleTool(状态模式)。在鼠标按下时,CircleTool 会想要创建一个新的 Circle。该工具应该直接创建一个新的CircleEntityController并调用类似的东西canvasController.addEntity(circleController)吗?那么创建 Circle 的模型和视图的责任应该在哪里呢?

对不起,如果这些问题有些模糊:)

--编辑--

这是我正在谈论的伪代码示例:

CircleTool {
    ...
    onCanvasMouseDown: function(x, y) {
        // should this tool/service create the new entity's model, view, and controller?
        var model = new CircleModel(x, y);
        var view = new CircleView(model);
        var controller = new CircleController(model, view);

        // should the canvasController's add method take in all 3 components
        // and then add them to their respective endpoints?
        this.canvasController.addEntity(model, view, controller);
    }
    ...
}


CanvasController {
    ...
    addEntity: function(model, view, controller) {
        // this doesn't really feel right...
        this.entityControllers.add(controller);
        this.model.addEntityModel(model);
        this.view.addEntityView(view);
    }
    ...
}
4

7 回答 7

18

哇,好吧,我可能对这个问题有一个令人惊讶的答案:我一直在抱怨 MVC 如何被认为是编程中完美的象征,没有人认为有任何问题。最喜欢的面试问题是“您在 MVC 中可能会遇到哪些问题或挑战?” 令人惊讶的是,这个问题经常以一种困惑、不安的表情迎接。

答案非常简单:MVC 依赖于多个消费者从单个共享模型对象满足其需求的概念。当各种观点提出不同的要求时,事情就真的开始走向地狱了。几年前有一篇文章,作者提出了分层 MVC 的概念。其他人在评论中出现并告诉他们,他们认为自己发明的东西已经存在:一种称为 Presentation-Abstraction-Controller (PAC) 的模式。您在模式文献中看到这一点的唯一地方可能是 Buschmann 的书,有时被称为五人帮,或 POSA(面向模式的软件架构)。有趣的是,每当我解释 PAC 时,我都会使用绘画程序作为完美的例子。

主要区别在于,在 PAC 中,表示元素往往有自己的模型,这就是 PAC 的 A:对于每个组件,您不必有,但可以有一个抽象。根据此处的其他一些响应,然后发生的是您有一个协调控制器,在这种情况下,它将统治画布。假设我们想在画布的一侧添加一个小视图,显示各种形状的数量(例如正方形 3、圆形 5 等)。该控制器将向协调控制器注册以监听两个事件:elementAdded 和 elementRemoved。当它收到每个通知时,它会简单地更新它在自己的抽象中的映射。想象一下,改变一堆组件用来添加对此类事物的支持的共享模型将是多么荒谬。此外,

分层部分是 PAC 中可以有多个控制器层:例如,Canvas 级别的协调控制器不会知道具有特殊行为的各种组件中的任何一个。我们可能会创建一个可以包含其他东西的形状,这需要逻辑来接受拖动等。该组件将具有自己的抽象和控制器,并且该控制器将与 CanvasController 协调其活动等。它甚至可能在它与包含的组件进行通信的某个点。

这是POSA书

于 2013-03-13T04:14:11.883 回答
1

有许多不同的方法来攻击它。我不同意这里发布的其他答案。

但是,我这样做的一种方法是使用观察者模式。让我解释一下原因。

观察者模式在这里是必要的,因为这些绘图工具没有画布就什么都不是。因此,如果您没有画布,则不能(或不应该)调用圆形工具。所以相反,我的画布上有许多观察者。

可以在画布上使用的每个工具都添加为可观察事件。然后,当一个事件被触发时——比如“开始绘制”——该工具作为上下文发送(在本例中为“圆圈”)。从那里开始执行圆形工具的动作。

另一种想象方式是,每一层都有自己的服务、模型和视图。控制器实际上位于外部级别并与画布相关联。因此,服务仅由其他服务或控制器调用。没有圆形工具控制器 - 所以只有另一个服务(在我们的例子中是观察到的事件)可以调用它。该服务负责聚合数据、构建模型和提供视图。

于 2013-03-09T01:51:34.050 回答
0

如果我是你,我会在处理形状时选择复合模式。不管你是否有圆形、正方形、矩形、三角形、字母等,我都会把所有东西都当作一个形状。一些形状可以是简单的形状,如线条,其他形状可以是更复杂的复合形状,如图形、饼图等。一个好主意是定义一个引用基本形状和高级(复杂)形状的基类。基本形状和高级形状都是同一类型的对象,只是高级形状可以有孩子来帮助定义这个复杂的对象。

通过遵循此逻辑,您可以使用每个形状绘制自己并且每个形状都知道其位置的逻辑,并基于此应用某些逻辑和算法来询问单击了哪个形状并且每个形状可以响应您的事件。

根据GoF 书

意图

将对象组合成树结构以表示部分整体的层次结构。Composite 让客户可以统一处理单个对象和对象的组合。

动机

绘图编辑器和原理图捕获系统等图形应用程序允许用户使用简单的组件构建复杂的图表。用户可以对组件进行分组以形成更大的组件。[...]

复合模式的关键是一个代表基元及其容器的抽象类。对于图形系统,这个类是Graphics。Graphic 声明了诸如 Draw 之类的特定于图形对象的操作。它还声明了所有复合对象共享的操作,例如访问和管理其子对象的操作。

现在,回到你的问题。如前所述,其中一个基本功能是Draw方法。该方法可以在具有以下签名的基类中实现为抽象方法:

public virtual void Draw(YourDrawingCanvas canvas);

每个形状都会实现它自己的 Draw 版本。您传递给它们的参数是对绘图画布的引用,每个形状都将在其中绘制自己。每个形状都可以在其内部结构中存储对自身的引用,并且可以用来将鼠标单击位置与这些坐标进行比较,以告知您单击了哪个形状。

于 2013-03-12T08:36:35.803 回答
0

RenderingService 由于缺乏更好的名称 - 管理形状交互的东西)将实例化新的Circle 域对象并通知它(直接或当视图请求新数据时)视图。

似乎您仍然习惯于将所有应用程序逻辑(存储抽象和域对象之间的交互)转储到表示层(在您的情况下 - 控制器)。

PS我假设你不是在谈论HMVC。

于 2013-02-22T14:38:29.370 回答
0

从我的角度来看,使用嵌套的 MVC 组件在这里有点矫枉过正:在每个时间点,模型都包含多个元素(不同的圆形、正方形等,它们可能是使用复合模式的嵌套构造,如另一个答案中所述) . 但是,显示元素的画布只是一个视图!(并且对应于单个视图,只需要一个控制器。)

具有多个视图的一种情况可能是元素列表(例如,显示在画布旁边) - 然后您可以将画布和元素列表实现为同一个模型上的两个不同视图。

关于如何“最好”实现添加元素的问题:我会考虑以下事件序列:

  • 视图通知其侦听器已绘制了一个新的圆形元素(例如,以中点和初始半径作为参数)。
  • 控制器注册为视图的监听器,因此调用控制器的“draw-circle(point, radius)”监听器方法。
  • 控制器在模型中创建一个新的圆实例(直接或通过作为模型一部分的工厂类 - 我认为有很多不同的方式来实现新元素的创建)。控制器是“在控制中”(字面意思),所以我相信实例化一个新元素(或至少触发实例化)是控制器的责任
  • 在模型中,上一步调用了某种“添加元素”方法。
  • 该模型向其所有侦听器发出“新元素创建”通知(可能传递对新创建元素的引用)。
  • 画布被注册为模型的监听器,因此画布的“新创建的元素(元素)”监听器方法被调用。
  • 作为对后一个通知的响应,画布(在其自身上)绘制圆圈。
于 2013-03-12T18:09:54.373 回答
0

这只是一个想法,但请考虑中介者模式是否适用。

来自四人组

意图

定义一个对象,该对象封装一组对象如何交互。Mediator 通过阻止对象显式地相互引用来促进松散耦合,并且它允许您独立地改变它们的交互。

适用性

在以下情况下使用中介者模式

  1. 一组对象以定义明确但复杂的方式进行通信。由此产生的相互依赖是非结构化的并且难以理解。
  2. 重用一个对象很困难,因为它引用了许多其他对象并与之通信。
  3. 分布在多个类之间的行为应该是可定制的,无需大量子类化。

结果

中介者模式有以下优点和缺点:

  • 它限制了子类化。中介者将原本会分布在多个对象之间的行为本地化。改变这种行为只需要继承 Mediator;同事类可以按原样重复使用。
  • 它使同事解耦。中介促进同事之间的松散耦合。您可以独立地改变和重用 Colleague 和 Mediator 类。
  • 它简化了对象协议。中介者用中介者与其同事之间的一对多交互来代替多对多交互。一对多关系更容易理解、维护和扩展。
  • 它抽象了对象如何协作。使调解成为一个独立的概念并将其封装在一个对象中,可以让您专注于对象如何与它们的个体行为之外的交互。这可以帮助阐明对象在系统中的交互方式。
  • 它集中控制。中介者模式用交互的复杂性换取中介者的复杂性。因为中介者封装了协议,所以它可能比任何单独的同事都复杂。这会使调解器本身成为难以维护的整体。
于 2013-03-07T19:55:38.067 回答
0

我假设类似 MSPaint 的行为,其中活动工具创建一个矢量图形字形,用户可以对其进行操作,直到他满意为止。当用户满意时,字形被写入图像,这是一个像素光栅。


当圆形工具被选中时,我会让 CanvasController 取消选择先前活动工具的 MVC 三重奏(如果另一个工具处于活动状态)并创建一个新的 CircleToolController、CircleModel 和 CircleView。先前活动的字形变为最终字形并将其自身绘制到 CanvasModel。

CanvasView 需要传递对 CircleView 的引用,以便它可以在绘制 Circle 之前将 CanvasModel 的像素绘制到屏幕上。将圆圈实际绘制到屏幕上,我将委托给 CircleView。

因此,CircleView 将需要了解和观察除 CircleModel 之外的其他更通用的模型类,我正在考虑颜色选择/调色板模型,以及填充样式和线条粗细等模型。这些其他模型寿命一样长就像应用程序一样,并且有自己的视图和控制器。毕竟,它们与应用程序的其余部分完全不同。


作为旁注:您实际上可以将 CanvasView 的 CanvasModel 绘制(像素颜色的光栅)从整个屏幕更新的协调中分离出来。有一个更高级别的 PaintView,它知道 CanvasView 和活动 GlyphView(例如 CircleView)协调 CanvasView 和 GlyphView 之间的绘图。

于 2013-03-12T23:39:24.253 回答