了解MVC设计模式

43

我对MVC模式理解上有些困惑。我确实理解我们试图将GUI与业务逻辑解耦,但我不明白如何做到这一点。

据我所了解,View是用户所看到的内容。因此,它通常是窗口/表单。 Controller位于ViewModel之间。控制器将使数据在两个方向上“流动”。当需要时,它还会保持状态(如果我有一个包含5个步骤的向导,则由Controller负责确保按正确顺序完成等)。Model是应用程序逻辑核心所在的地方。

这个理解正确吗?

为了尝试将其转化为更有意义的内容,我将尝试使用WinForms进行简单示例的草图(请勿使用ASP.NET或WPF!-对于Java群体而言,从我所了解的情况来看,Swing的工作方式与WinForms类似),以查看我的理解是否正确,并提出我经常遇到的问题。


假设我有一个包含一个类的模型(只是为了让它更容易。我知道这样会让示例看起来很愚蠢,但这样更容易),

class MyNumbers {
    private IList<int> listOfNumbers = new List<int> { 1, 3, 5, 7, 9 };

    public IList<int> GetNumbers() {
        return new ReadOnlyCollection<int>(listOfNumbers);
    }
}

现在是时候制作我的Controller了:

class Controller
{
    private MyNumbers myNumbers = new MyNumbers();

    public IList<int> GetNumbers() {
        return myNumbers.GetNumbers();
    }
}

View应该只有一个ListBox,其中包含从MyNumbers中检索到的所有数字。

首先,第一个问题:

Controller是否应该负责创建MyNumbers?在这种简单情况下,我认为这是可以接受的(因为MyNumbers将完全相同,无论如何,并且没有关联状态)。 但是假设我想要为应用程序中所有不同的控制器使用相同的MyNumbers实例。 我需要将我想要使用的那个MyNumbers实例传递给此Controller(以及所有其他需要它的人)。 谁将对此负责?在WinForms示例中,这是View吗?还是创建View的类?

反过来问:这3个部分的实例化顺序是什么?“MVC”的“所有者”调用了哪些代码来创建它?
Controller应该创建ViewModel吗?
或者View应该实例化Controller,而Controller实例化Model吗?

第二个问题:

如果我只想让我的应用程序具有此Controller所代表的Use Case,那么main方法应该是什么样子的?

第三个问题:

为什么在下面的MVC图中,View有一个指向Model的箭头?
Controller不应该始终是ViewModel之间的桥梁吗?

alt text


我可能还有一两个问题,但在理解这个第一个细节后提出它们可能会更有意义。或者也可能是在理解第一个问题后,所有其他问题都会迎刃而解。

谢谢!


2
好问题 - 我仍在努力理解如何“正确地”实现MVC。 - RQDQ
6
这个问题为什么被认为是"主观且有争议的"?:boggle: - Jeff Sternal
这是一个基于引用图示的例子:http://stackoverflow.com/questions/3072979 - trashgod
10个回答

26

理解MVC最简单的方法是在强制使用它的框架中使用它,话虽如此...

  • 模型与数据源(数据库或其他)交互,并使您可以访问数据。
  • 视图与外部世界交互,接收来自某处的输入并将数据传递给控制器;它还侦听控制器以确保正确显示数据。
  • 控制器是所有魔法发生的地方;控制器操作数据,推动事件,并处理在两个方向(从/到视图和从/到模型)的更改。

这张图非常有帮助(比维基百科更容易理解):MVC Diagram

来源,以及一篇很棒的MVC相关文章!


2
很好的回答(+1)。你能提供一下这个图表的来源吗? - Cam
1
@BalusC:这两个图表是不同的。第一个可能有更多的信息,但讨论的是一种较旧的MVC实现方式,所以我认为第二个更有帮助。 - Cam
2
控制器将引用ListBox并在事物发生变化时更新模型。(视图从不存储任何内容,它只发送消息) - xj9
@indieinvader:啊!那就是缺失的其中一部分。我一直认为这应该由视图(View)来完成。 - devoured elysium
1
这个 Oracle 链接无效。有人能在这里传递一下吗?谢谢。 - amit kohan
显示剩余4条评论

3
关于我帖子中的批评,我想发一篇关于如何在PHP中创建MVC模式的文章。在PHP中,我将框架分成几个部分,其中一些是MVC中常见的部分。
主要部分: - 控制器 - 模型 - 视图
次要部分: - 模型层 - 视图加载器 - 库 - 错误层
在控制器中,我通常允许所有对次要层以及来自主要层的视图和模型的访问。
以下是我的结构方式:
|---------|       |------------|       |------------|
| Browser | ----> | Controller | ----> |   Model    |
|---------|       |------------|       |------------|
     |                  |   |                |
     |                  |   |----------------|
     |                  |
     |            |------------|
     -------------|    View    |
                  |------------|

我的图表通常会绕过 View <-> Model 的连接,而是使用 Controller <-> Model,然后从 Controller <-> View 的链接中分配数据。
在我的框架内,我倾向于创建一个对象存储系统,以便可以轻松地获取对象等。我的对象存储的示例如下所示。
class Registry
{
   static $storage = array();

   public static function get($key)
   {
       return isset(self::storage[$key]) ? self::storage[$key] : null;
   }

   public static function set($key,$object)
   {
       self::"storage[$key] = $object;
   }
}

这是稍微高级一些的内容,但大致上是这样的概述。当我第一次初始化对象时,我会像这样存储它们:Registry::set("View",new View());,以便随时访问。

所以,在我的控制器中,也就是基础控制器中,我创建了几个魔术方法__get()__set(),以便任何扩展控制器的类都可以轻松地返回请求,例如:

abstract class Controller
{
   public function __get($key)
   {
       //check to make sure key is ok for item such as View,Library etc

       return Registry::get($key); //Object / Null
   }
}

用户控制器

class Controller_index extends Controller
{
    public function index()
    {
       $this->View->assign("key","value"); // Exucutes a method in the View class
    }
}

该模型也将被放置到注册表中,但只允许从ModelLayer调用。

class Model_index extends ModelLayer_MySql
{
}

或者

class Model_index extends ModelLayer_MySqli
{
}

或文件系统

class Model_file extends ModelLayer_FileSystem
{
}

以便每个类都可以针对特定的存储类型。

这不是传统的MVC模式,但可以称之为适应性MVC。

其他对象(例如视图加载器)不应该被放入注册表中,因为它们并非专门为用户利益而设计,而是被其他实体(如视图)使用。

abstract class ViewLoader
{
   function __construct($file,$data) //send the file and data
   {
       //Include the file and set the data to a local variable
   }

   public function MakeUri()
   {
       return Registry::get('URITools')->CreateURIByArgs(func_get_args());
   }
}

由于模板文件被包含在视图加载器中而不是视图类中,因此它将用户方法与系统方法分离,并允许在视图本身中使用方法进行一般逻辑处理。

模板文件的示例。

<html>
   <body>
      <?php $this->_include("another_tpl_file.php"); ?>
      <?php if(isset($this->session->admin)):?>

          <a href="<?php echo $this->MakeUri("user","admin","panel","id",$this->session->admin_uid) ?>"><?php echo $this->lang->admin->admin_link ?></a>

      <?php endif; ?>
   </body>
</html>

我希望我的示例能够帮助您更好地理解。

2

回答第三个问题:

当模型变化时,它会通知视图,然后视图使用其getter方法从模型获取数据。


这是一个疑问句还是陈述句? - hvgotcodes
2
但是我们是否在视图和模型之间增加了不必要的耦合?我们应该只使用控制器作为一种代理吗?谁有将IList<int>数据转换为ListBox中的项目的责任?视图还是控制器?控制器应该向视图显示IList,还是例如具有对ListBox的引用并自己向其中添加内容?谢谢。 - devoured elysium
让我们看一个例子: 你有重要的数据,想要展示给用户。所以你有一个模型,可能是一个列表。视图从模型获取列表,并且视图知道如何展示数据。视图可以在表格、图表等形式中展示它。 - Zoltan Balazs
当您允许用户修改模型(添加/删除项目)时,您需要一个按钮或其他东西,并调用控制器的其中一种方法,控制器将修改模型。模型更改后,通知视图,视图获取数据。 - Zoltan Balazs

1
这段内容来自Java,但希望它能有所帮助。
对于主函数:
public static void main(String[] args) 
{
       MyNumbers myNums = new MyNumbers();  // Create your Model
       // Create controller, send in Model reference.      
       Controller controller = new Controller(myNums); 
}

你的控制器需要引用你的模型。在这种情况下,控制器实际上创建了所有的Swing组件。对于C#,你可能想在这里留下表单初始化,但是View / Form需要引用Model(myNums)和Controller(controller)。希望一些C#人员可以在这方面提供帮助。View还需要注册为Model的观察者(请参见Observer Pattern)。

这是我拥有的构造函数(根据你的情况进行调整):

public NumberView(Controller controller, MyNumbers myNums)
{
      this.controller = controller; // You'll need a local variable for this
      this.myNums = myNums; //You'll need a local variable for this
      myNums.registerObserver(this); // This is where it registers itself
}

视图将工作传递给控制器来处理用户的操作(按钮等)。控制器决定在模型中调用/执行什么。通常情况下,模型会执行某些操作并更改其状态(也许是您列表中的更多数字。它所做的任何事情)。此时,模型将让其观察者知道它已经发生了变化,并更新自己。然后视图获取新数据并更新自身。这就是为什么模型和视图会交流(您的第三个问题)。

因此,模型将具有:

public void notifyObservers()
{
    for (Observer o: observers)
    {
        o.update();  // this will call the View update below since it is an Observer
    }
}

所以在视图中,你会有类似这样的内容:

public void update()
{
    setListBox(myNums.getNumbers());  // Or whatever form update you want
}

希望能帮到你。我知道这是 Java 相关的,但是理念仍然适用。你需要稍微阅读一下观察者模式的相关内容才能完全掌握它。祝你好运!

1
抱歉,我提前按下了回车键。希望完整的答案能够帮到您。如果您需要任何进一步的解释,请告诉我。 - Awaken
所以,首先,我理解你指的是“MVC 1”,就像我原始帖子中的图表所示,而不是indieinvader的图表,对吗? - devoured elysium
啊,我从来没有注意到《Head First 设计模式》有一个关于 MVC 的章节! - devoured elysium
哈哈。是的,在复合模式部分的后面。 - Awaken
1
我实际上不使用Java库中的Observable类(因为继承问题)。我创建了自己的接口叫做Observable(和Observer),它几乎是一样的东西。当我在互联网上发布时,这会让人感到困惑。希望这有助于使它看起来不那么“糟糕”。它确实运行得非常好。 - Awaken
显示剩余3条评论

1
“据我所理解,视图(View)是用户所看到的内容。通常它是窗口/表单。控制器(Controller)位于视图和模型之间。控制器将在两个方向上“处理”数据。当需要时,它还会持久化状态(例如,如果我有一个包含5个步骤的向导,那么控制器的责任就是确保这些步骤按正确的顺序进行)。模型(Model)是我的应用程序逻辑核心所在的地方。”
“这几乎是正确的。控制器不会持久化数据。它调用一个持久化数据的服务。原因是,持久化数据永远不只是一个保存的调用。您可能希望对数据进行验证检查,以确保根据业务需求它是合理的。您可能希望进行一些身份验证,以确保用户可以保存数据。如果您在服务中执行此操作,则可以获得一个很好的功能捆绑包,可以在Web应用程序和Web服务中重复使用。如果您在控制器中执行此操作,例如在Web应用程序中,当您编写Web服务时,您将不得不重构和/或复制代码。”
“针对您的评论“我不确定我完全理解了您的观点。控制器检查UI输入,还是模型检查?”。”
你的控制器应该只控制哪些业务功能路径被执行。这就是它的作用。控制器应该是编写代码最容易的部分。你可以在 GUI(即视图)上进行一些验证,例如确保电子邮件地址格式正确、文本输入不超过最大值等,但业务层也应该验证输入——因为我之前提到的原因,当你开始建立更多的端点时,你不必重构代码。

当我说“handle”时,使用了不正确的术语。我的意思不是要“处理”它,而只是让它从视图流向控制器,再从控制器流回视图。 - devoured elysium
我不确定我完全理解了你的观点。是控制器检查UI输入,还是模型检查? - devoured elysium
1
当然。我认为你明白了。我的评论更多的是关于如何使用MVC与一些常见的设计选择,使您能够编写可重复使用的组件。如果您的模型处理视图问题(如输出HTML),或者您的控制器处理业务逻辑(如决定如何格式化要保存的值),等等,那么您就知道做错了。 - hvgotcodes

1

控制器是否应该负责创建MyNumbers?

我会说“绝对不是”。如果MVC模式旨在解耦M、V和C元素,那么如果C仅使用new MyNumbers()实例化M,这怎么能行呢?在Java中,我们会在这里使用类似Spring Framework的东西。您需要一种方法来表达依赖关系 - 或者更确切地说,它如何得到满足 - 在配置文件或其他合适的位置(即不在编译代码中)。

但是这个问题还有另一个方面:您可能不应该使用您打算使用的具体运行时类型来定义C内部的myNumbers变量。使用接口或抽象类,并将其保持开放以确定实际运行时类型。这样,在未来,您可以重新实现IMyNumbers接口以满足新出现的要求(您今天不知道的要求),并且您的C组件将继续完美地工作,毫不知情。


1
你提出了很好的观点。在我理解如何自己实现之前,我想要远离框架!只有在此之后,我才会寻找框架。 - devoured elysium
@devoured elysium -- 当然可以!我知道你提供了一个非常简单的例子,并且想要讨论它。但是在这种情况下,简单的例子可能会误导人,如果它们太简单以至于甚至没有动力使用MVC模式。在现实世界的场景中,有足够的理由使用像MVC这样的模式。 - Drew Wills

1
在以下的MVC图表中,为什么视图会有一个指向模型的箭头?难道控制器不应该永远是连接视图和模型之间的桥梁吗?
这是MVC模型2。通常在Java企业应用程序中看到它,其中控制器(CONTROL)既处理来自/发送到模型(MODEL)的业务数据,还选择要呈现回客户端的视图(VIEW)。在呈现给客户端时,视图将使用模型中的数据。
这里有一个例子,演示如何从JSP(视图)文件中访问bean(模型)中的数据: alt text
(来源:blogjava.net
class Person {String name;} // MODEL
My name is ${bean.name}     // VIEW

1

我会尽力从相对不太技术化的角度回答这个问题。我将尝试通过一个一般性的例子来解释。

控制器 控制着使用哪个 视图。所以,假设你正在向页面写入内容,控制器 将引导你到 输入视图(例如),而如果你正在阅读同一页,则它将引导你到它的 成功视图(例如)。

在向页面写入内容后,控制器 将把这些参数传递给相关的 模型,其中包含了处理这些参数的逻辑。如果出现错误,则 控制器 将引导你到 错误视图

我的知识基于我使用 Agavi 的一个月经验。希望这可以帮助到你。


最终用户始终查看各种视图。 - Jungle Hunter

0
视图绘制模型并将其呈现给用户;控制器处理用户输入并将其翻译为对模型的修改;模型保存数据和修改逻辑。没有必要将其翻译成代码,因为你不可能一次性就正确地得到它。

0

视图

  • 用户界面/负责输入输出/一些验证/需要有一种方式通知外部世界UI级别事件

  • 仅了解模型

模型

  • 数据结构/表示呈现的数据/不应包含业务逻辑(最多只有一些数据/结构验证)

  • 仅知道自己(想象一个只有姓名和年龄的Person类)

控制器

  • 负责业务逻辑/创建视图并将相应的模型粘合在一起/必须能够响应视图事件/访问应用程序的其他层(持久性/外部服务/业务层等)

  • 知道所有内容(至少是视图和模型),负责将所有内容粘合在一起


我不明白为什么模型不应该包含任何业务逻辑。难道不应该让模型拥有所有的业务逻辑吗? - devoured elysium
该模型应该跨越应用程序逻辑的多个层次(数据访问、业务、UI等)。此外,同一个模型(例如具有所有相关数据的人员)可以在多个业务场景中使用。因此,将模型与不同的业务交易/上下文分离是一种良好的实践。 - Adrian Zanescu

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