MVC模式。模型、视图和控制器之间的关系

9

我对Model、View和Controller之间的关系感到困惑。

这个主题展示了从View到Controller的箭头,从Controller到Model的箭头以及从Model到View的箭头: http://www.codeproject.com/Tips/31292/MVC-v-s-MVP-How-Common-and-How-Different

然而,这个主题展示了Model和View之间的双向箭头;View和Controller之间的双向箭头;以及从Controller到Model的箭头: http://www.codeproject.com/Articles/288928/Differences-between-MVC-and-MVP-for-Beginners

最后,这个主题展示了从View到Model的箭头,从Controller到Model的箭头以及从Controller到View的箭头: http://www.w3schools.com/aspnet/mvc_intro.asp

我有一些问题:

  1. 哪些关系是正确的?
  2. 业务逻辑应该在Controller或Model中处理?我在某处读到过业务逻辑不应该放在Controller中(ASP.Net MVC)
  3. 如果控制器将一个对象传递给视图,这个对象属于Model吗?
  4. 视图如何直接从Model检索数据?它是否直接引用Model或与来自Controller的Model进行交互?
2个回答

9
我发现你链接的所有图片都很令人困惑。这张图片(取自维基百科)最能清晰地解释它。

MVC diagram

工作原理

MVC考虑了三个角色。模型是表示领域信息的对象。它是一个非可视化对象,包含除用于UI之外的所有数据和行为。

视图代表模型在UI中的显示。因此,如果我们的模型是客户对象,我们的视图可能是一个充满UI小部件的框架或一个使用模型信息呈现的HTML页面。视图仅涉及信息的显示;对信息的任何更改都由MVC三位成员中的第三位处理:控制器。控制器接受用户输入,操作模型,并导致视图适当地更新。这样UI就是视图和控制器的组合。

-- 引用自Martin Fowler的《企业应用架构模式》Patterns of Enterprise Application Architecture

您的问题

  1. MVC关注的是关注点分离,而不是关系。
  2. 业务逻辑应该在模型中。控制器仅用于与用户交互。
  3. 是的(很可能)
  4. 视图从模型中获取必要的信息。使用被动视图时,对象(来自模型)从控制器传递。重要的是,视图只从模型中读取,永远不会写入/更新它。

    视图观察并响应模型中的更改。模型是领域模型,而不是单个记录集或实体。

勘误

MVC在现今的常见用法与Martin Fowler最初提出的MVC模式有所不同。他是基于Smalltalk语言提出该模式的。

MVC的核心,以及对后来的框架影响最大的想法,是我所谓的分离表示

MVC的另一个部分是模型、视图和控制器之间的交互。

在这种情况下,所有的视图和控制器都观察模型。当模型发生变化时,视图会做出反应。

这与由Ruby on Rails流行的MVC非常不同,其中控制器负责准备和加载视图。

class ArticlesController < ApplicationController
  def create
    @article = Article.new(article_params)

    if @article.save
      redirect_to @article
    else
      render 'new'
    end
  end

Martin Fowler将MVC简化为以下几点:

  • 在表示层(视图和控制器)和领域层(模型)之间建立强大的分离 - 分离表示。
  • 将GUI小部件分为控制器(用于响应用户刺激)和视图(用于显示模型状态)。控制器和视图不应直接通信,而是通过模型进行通信。
  • 使视图(和控制器)观察模型以允许多个小部件更新而无需直接通信 - 观察者同步。

-- 引自Martin Fowler的GUI Architectures


关于你提到的第四点:实际上,通常情况下(正如你的图像和引用所示),控制器不会将对象传递给视图,而是视图从模型中获取必要的信息。这可能是由控制器的操作引起的,也可能是视图直接监听模型的变化。只有在“被动视图”的版本中,控制器才会将数据传递给视图。 - jhyot
@jhyot 谢谢,已修改。 - Arnold Daniels
2
最近阅读了马丁·福勒(Martin Fowler)关于MVC的文章。事实上,维基百科上的图片是错误的。在经典的Smalltalk MVC中,视图是模型的观察者。文章链接:https://martinfowler.com/eaaDev/uiArchs.html - Oleksandr Papchenko
请问您为什么划掉了第四个问题的答案? - Jan Turoň
虽然 MVC 经常被实现成那样,但这并不是 Fowler 最初描述的方式。 - Arnold Daniels
@OleksandrPapchenko:不过,“更新”箭头不能被理解为观察者关系吗?毕竟,可观察对象会“更新”它的观察者,因此模型确实会“更新”已连接的视图——只是它并不知道它们是视图,只知道它们是事件接收器。 - The_Sympathizer

8

维基百科不是技术手册

enter image description here

维基百科的MVC图像存在许多混淆。问题在于,在UML类图中,A -> B表示A是活动的并调用B(因此A依赖于B)。但是维基社区不是纯技术性的,并且画出带有未解释和随意箭头含义的图像。拥有一个完整的箭头圆圈看起来很好,很合理,对吗?

不对。最有趣和最明显的荒谬之处是View --sees--> User:这是什么意思?大兄弟小心翼翼地用网络摄像头跟踪用户?这个含义表明箭头方向被颠倒了,但是我们将没有那个漂亮的生命之环...

但是认真地说:图像最令人震惊的设计缺陷是Model --updates--> View关系。从技术上讲,它总是需要View --reads--> Model。如果Model是活动的,那么数据对象和操作将依赖于系统的其他部分,因此它将无法重用。

另一个荒谬之处是User --uses--> Controller。控制器是不可见的,用户只使用View,其他部分对他们来说是黑盒子。 控制器基本上是事件实现系统。事件的源可以是用户的输入或模型的数据更改,但是它们使用接口,而是控制器实现它们。 (这被称为反向控制,由于它有些人混淆了箭头的方向。)这些操作命令Model和View进行更新,因此箭头指向Controller。没有东西控制Controller。这就是为什么它被称为Controller:所有控制都聚合到其中。主动视图是一个例外:如果需要填充其选择框,则它可以在没有Controller的祝福下读取Model。但是有时View对Model接口的依赖是不希望的,因此并非所有View都设计为主动。

因此,正确的图像是:

      --- Controller ----
      |                 |     !!! arrows mean dependency, not data flow !!!
      V                 V
    View ------------> Model
         (if active)

事件调度器

这是维基百科引起混淆的根本原因:MVC架构不能独立运行,需要事件调度器来处理事件并调用控制器:

  • 在Web应用程序中,它是HTTP服务器
  • 在托管应用程序中,它是IDE自动生成的代码(通常对编码人员隐藏)
  • 在本地应用程序中,它位于主循环中
  • 在较低级别的应用程序中,它由系统环境提供
  User <--> View --> Event Dispatcher
              ∧            |
              |            |        !!! arrows mean data flow, not dependency !!!
Model <--> Controller <-----

现在我们有交互循环。请注意箭头的含义:从依赖性的角度来看,事件调度器当然是独立于控制器的,而控制器需要一些事件调度器。
要实现MVC,我们需要理解以下活动模型方案中描述的依赖注入技术。
活动模型场景
有时候,模型也可以是事件的来源,例如,如果某个账户信用额度降至某个水平,它可以发出信号通知视图显示警告。但是模型应该独立于系统的其他部分,因此它不能调用视图。观察者设计模式是实现它的方法,请参见下面的简化示例:
模型
模型使用接口让视图钩住其“帐户过低”或“帐户过高”的事件。
interface AccountObserver {
    // in dummy examples, these methods are often vaguely named update()
    public void accountLow(int value);
    public void accountHigh(int value);
}

class Model {
    // protected, not private, to make the Model extensible
    protected int account;
    protected AccountObserver observer;

    // more observers should be allowed, we should have array of observers
    // and name the method "register..." instead of "set..."
    public void setAccountObserver(AccountObserver o) {
        observer = o;
    }

    public void updateAccount(int change) {
        account+= change;
        // calculate values ...
        if(account<minValue) observer.accountLow(account);
        if(account>maxValue) observer.accountHigh(account);
    }
    ...
}

视图

有些人建议聚合观察者而不是实现它。继承更简单,如果模型以一种方式定义了所有观察者,使得它们的方法具有唯一的名称,我们可以继承。

class View : AccountObserver {
    public void accountLow(int value) {
        warning("Account too low! It has only "+value+" credits!");
    }
    public void accountHigh(int value) {
        warning("Account too high! It has above "+value+" credits!");
    }
    ...
}

控制器

在架构的控制器部分,我们将用户界面(视图)和其他事件源与模型(可能由多个数据源组成)结合在一起。在最简单的情况下:

class Controller {
    protected Model model;
    protected View view;

    public Controller(Model model, View view) { // Constructor
        this.model = model; this.view = view;
        model.setAccountObserver(view);
    }

    // called by Event Dispatcher
    void onUpdateAccount(int requestedValue) {
        if(requestedValue<0) ... // the business logic can be here or in the Model
        model.updateAccount(requestedValue); // this updates the View
    }
}

注意model.setAccountObserver(view) - 模型和视图对象(作为控制器的属性)是耦合的,但是模型和视图是独立的。这种依赖注入模式是理解模型 - 视图关系的关键。

现在回答您的问题

  1. 哪些关系是正确的?全部和没有。 全部,因为差异来自于箭头不同的含义。 没有,因为它们的箭头含义没有明确描述(或者像维基百科的图片一样错误,请参见Olexander Papchenko的评论)。
  2. 业务逻辑应该由控制器或模型处理?两者兼顾。纯数据操作肯定属于模型,但模型不能决定一切,例如用户何时以及如何登录。这就属于控制器,并且在下面的代码中显示。
  3. 如果控制器将一个对象传递给视图,这个对象属于模型吗?是的,请参见下面的代码。
  4. 视图如何直接从模型检索数据?它直接引用模型还是与来自控制器的模型交互?我认为这两种情况都是可能的:如果视图需要显示一些静态数据,例如国家列表,如果它拥有模型实例(也许通过某个接口),并直接调用其getter方法,则不会出现问题(如果视图需要更改数据,则创建事件并让控制器处理)。这就是上面图片中的箭头View --> Model。如果数据是动态的,例如getContacts(int userId),则需要控制器验证请求:
class Controller {
    protected Model model;
    protected View view;
    protected User user;

    public Controller(Model model, View view) { // Constructor
        this.model = model; this.view = view;
        model.setAccountObserver(view);
        initBusinessLogic();
    }

    protected function initBusinessLogic() {
        user = view.loginModalDialog(); // active View (needs to get userId from Model)
        // passive View alternative
        // [login, password] = view.loginModalDialog();
        // user = model.authenticateUser(login, password);

        // Controller pass object from the Model to the View
        if(user.isLoggedIn()) view.setContactList(model.getContacts(user.id));
        // if(user.isLoggedIn()) view.setContactList(userId); // less universal
        // view.doYourStuff(userId); // wrong, View should not have business logic
    }
}

注意,模型和视图通常是作为具有不同职责的多个类实现的(视图用于对话框、主页面等,模型用于用户操作、订单等)。控制器每个应用程序只有一个;如果我们为每个视图有专门的控制器,那么它被称为Presenter,并且该架构为MVP。


1
视图还可以在控制器之外注册其模型,并直接调用模型的方法。希望您指的是getter,如果不是...我会在一点光明之后陷入黑暗... - Botea Florin
@BoteaFlorin 您说得对,非常好的观点。在良好的代码分离中,视图只能调用getter来设置其控件(例如使用数据填充选择框),其他任何事情都不是它关心的。如果视图想要调用模型的某些setter(例如单击提交按钮),它只需创建一个事件,控制器决定要做什么(即控制器检查当前状态下事件是否有效)。在Web应用程序中,控制器处理所有HTTP请求,视图创建它们(请求也可以来自其他端点),而HTTP服务器则充当事件分发器。 - Jan Turoň
@BoteaFlorin 顺便说一下,事件分发器不是控制器的一部分。如果您的应用程序不由HTTP服务器管理,则事件分发器通常位于主循环中。如果您在某些IDE(如MSVC)中使用可视化编程,则调度程序和主循环是自动生成的,即通过调用类似于Application.Start(new Form1())的内容来调用Form1实例中的事件处理程序。 - Jan Turoň

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接