关注点分离; MVC;为啥?

Posted

技术标签:

【中文标题】关注点分离; MVC;为啥?【英文标题】:Separation of concerns; MVC; why?关注点分离; MVC;为什么? 【发布时间】:2010-10-12 23:34:40 【问题描述】:

在开始我的下一个主要项目之前,我目前正在阅读 OO。为了给您一些快速的背景知识,我是一名 php 开发人员,从事 Web 应用程序的工作。

我特别感兴趣的一个领域是用户界面;具体如何构建它并将其连接到我的面向对象“模型”。

我一直在阅读这方面的内容。我的最爱之一是: Building user interfaces for object-oriented systems

“所有对象都必须提供自己的 UI”

考虑 我的 问题,我可以看到它运作良好。例如,我构建我的“用户”对象来代表已经登录到我的网站的人。我的一种方法是“display_yourself”或类似的。我可以在我的代码中使用它。也许从这开始只是他们的名字。稍后,如果我需要调整以显示他们的名字+小头像,我可以更新这个方法,嘿-presto,我的应用程序更新了。或者,如果我需要将他们的名字作为他们个人资料的链接,我可以从一个地方轻松地再次更新。

就OO系统而言;我认为这种方法效果很好。查看其他 *** 线程,我在“关注点分离”下找到了这个: Soc

“在计算机科学中,分离 关注点 (SoC) 是 将计算机程序分解成 重叠的不同特征 功能尽可能少。一种 关注是任何利益或 专注于一个程序。通常, 关注点是功能的同义词 或行为。 SoC 的进展是 传统上通过 模块化和封装,与 信息隐藏的帮助。”

在我看来,我已经做到了。我的用户对象隐藏了它的所有信息。在显示之前,我的代码中没有任何地方可以说 $user->get_user_name()。

但是,这似乎与其他人认为的“最佳实践”背道而驰。

引用同一问题的“选定”(绿色)答案:

"关注点分离是保持 每个问题的代码 分离。改变界面 不应该需要改变 业务逻辑代码,反之亦然。 模型-视图-控制器 (MVC) 设计 模式是一个很好的例子 更好地分离这些问题 软件可维护性。”

为什么这会提高软件的可维护性?当然,使用 MVC,我的视图必须对模型非常了解?有关这一点的详细讨论,请阅读 JavaWorld 文章: Building user interfaces for object-oriented systems

不管怎样……终于到了真正的重点!

1. 谁能推荐任何详细讨论此问题的书籍?我不想要一本 MVC 书;我没有在 MVC 上出售。我想要一本讨论 OO / UI、潜在问题、潜在解决方案等的书。(可能包括 MVC) 亚瑟瑞尔的Object-Oriented Design Heuristics

涉及到它(也是一本很棒的书!),但我想要更详细的东西。

2. 谁能提出一个像 Allen Holub 的 JavaWorld 文章解释为什么 MVC 是一个好主意一样得到充分解释的论点?

非常感谢任何可以帮助我就此得出结论的人。

【问题讨论】:

很好的问题,基于提交的内容 - 引起人们思考和解释。 看看这个anagloy是否有帮助:zenofcoding.com/2009/01/06/another-way-to-think-about-mvc ...我认为 mvc 不是必须具备的,但它肯定是考虑分离逻辑的好方法。你总是有一个与数据库“M”交互的部分,以及一个与用户交互的部分“V”。因此,要将它们粘合在一起,您通常需要某种“C”——控制器。我读过的很多关于这个主题的东西往往使事情变得过于复杂。 【参考方案1】:

我的 2c .. 除了所说的之外,您还可以做的另一件事是使用用户对象的装饰器。这样,您可以根据上下文以不同方式装饰用户。所以你最终会得到 WebUser.class、CVSUser.class、RSSUser.class 等。

我并没有真正这样做,它可能会变得混乱,但它有助于避免客户端代码不得不从您的用户中提取大量信息。研究起来可能很有趣;-)

Why getter and setter methods are evil (JavaWorld) Decorator pattern

【讨论】:

【参考方案2】:

这是我在 PHP 中使用 MVC/关注点分离模式创建网站时采用的方法:

我使用的框架包含三个基本部分:

模型 - PHP 类。我向它们添加方法来获取和保存数据。每个 模型代表系统中不同类型的实体:用户、页面、 博文 视图 - Smarty 模板。这是 html 所在的位置。 控制器 - PHP 类。这些是应用程序的大脑。通常 站点中的 url 调用类的方法。 example.com/user/show/1 会 调用 $user_controller->show(1) 方法。控制器取出数据 模型并将其提供给视图。

这些作品中的每一个都有特定的工作或“关注点”。 模型的工作是为数据提供一个干净的接口。通常,站点的数据存储在 SQL 数据库中。我向模型中添加了用于获取数据和保存数据的方法。

视图的工作是显示数据。所有 HTML 标记都在视图中。处理数据表的斑马条纹的逻辑进入视图。处理日期显示格式的代码进入视图。我喜欢为视图使用 Smarty 模板,因为它提供了一些很好的功能来处理类似的事情。

控制器的工作是充当用户、模型和视图之间的中介。

让我们看一个例子,说明它们是如何结合在一起的以及好处在哪里:

想象一个简单的博客网站。主要数据是帖子。另外,假设该网站跟踪查看帖子的次数。我们将为此创建一个 SQL 表:

posts
id date_created title body hits

现在假设您想显示 5 个最受欢迎的帖子。以下是您可能会在非 MVC 应用程序中看到的内容:

$sql = "SELECT * FROM posts ORDER BY hits DESC LIMIT 5";
$result = mysql_query($sql);

while ($row = mysql_fetch_assoc($result)) 
    echo "<a href="post.php?id=$row['id']">$row['title']</a><br />";

这个 sn-p 非常简单,并且在以下情况下运行良好:

    这是您想要显示最受欢迎帖子的唯一地方 你永远不想改变它的外观 您永远不会决定改变什么是“热门帖子”

假设您想在主页上显示 10 个最受欢迎的帖子,并在子页面的侧边栏中显示 5 个最受欢迎的帖子。您现在需要复制上面的代码,或者将其放入包含逻辑的包含文件中以检查其显示位置。

如果您想更新主页的标记以向今天创建的帖子添加“新帖子”类怎么办?

假设您认为某个帖子很受欢迎,因为它有很多 cmets,而不是点击量。数据库将更改以反映这一点。现在,您的应用程序中显示热门帖子的每个位置都必须更新以反映新逻辑。

您开始看到一个复杂形式的雪球。很容易看出在项目过程中事情如何变得越来越难以维护。此外,还要考虑多个开发人员在一个项目上工作时的复杂性。在向输出中添加类时,设计人员是否必须咨询数据库开发人员?

采用 MVC 方法并在应用程序中实施关注点分离可以缓解这些问题。理想情况下,我们希望将其分为三个区域:

    数据逻辑 应用逻辑 和显示逻辑

让我们看看如何做到这一点:

我们将从模型开始。我们将有一个$post_model 类并给它一个名为get_popular() 的方法。此方法将返回一个帖子数组。此外,我们将给它一个参数来指定要返回的帖子数:

post_model.php

class post_model 
    public function get_popular($number) 
        $sql = "SELECT * FROM posts ORDER BY hits DESC LIMIT $number";
        $result = mysql_query($sql);
        while($row = mysql_fetch_assoc($result)) 
            $array[] = $row;
        
        return $array;
     

现在对于主页,我们有一个控制器,我们将其称为“主页”。假设我们有一个 url 路由方案,它在请求主页时调用我们的控制器。它的工作是获取热门帖子并将它们提供给正确的视图:

home_controller.php

class home_controller 
    $post_model = new post_model();
    $popular_posts = $post_model->get_popular(10);

    // This is the smarty syntax for assigning data and displaying
    // a template. The important concept is that we are handing over the 
    // array of popular posts to a template file which will use them 
    // to generate an html page
    $smarty->assign('posts', $popular_posts);
    $smarty->view('homepage.tpl');

现在让我们看看视图会是什么样子:

homepage.tpl   

include file="header.tpl"

 // This loops through the posts we assigned in the controller
 foreach from='posts' item='post' 
    <a href="post.php?id=$post.id">$post.title</a>
 /foreach

include file="footer.tpl"

现在我们有了应用程序的基本部分,并且可以看到关注点的分离。

模型关注的是获取数据。它了解数据库,了解 SQL 查询和 LIMIT 语句。它知道它应该交还一个不错的数组。

控制器知道用户的请求,他们正在查看主页。它知道主页应该显示 10 个热门帖子。它从模型中获取数据并将其提供给视图。

视图 知道帖子数组应该显示为一系列 achor 标签,后面带有 break 标签。它知道一个帖子有一个标题和一个ID。它知道应该将帖子的标题用于锚文本,并且应该在 href 中使用帖子 ID。视图也知道页面上应该显示页眉和页脚。

提及每件知道的内容也很重要。

模型不知道热门帖子显示在主页上。

控制器视图不知道帖子存储在 SQL 数据库中。

controllermodel 并不知道主页上帖子的每个链接后面都应该有一个中断标记。

因此,在这种状态下,我们在数据逻辑(模型)、应用程序逻辑(控制器)和显示逻辑(视图)之间建立了清晰的关注点分离。那么现在怎么办?我们采用了一个简短的简单 PHP sn-p 并将其分解为三个令人困惑的文件。这给了我们什么?

让我们看看关注点分离如何帮助我们解决上述问题。重申一下,我们希望:

    在子页面的侧边栏中显示热门帖子 使用额外的 CSS 类突出显示新帖子 更改“热门帖子”的基本定义

为了在侧边栏中显示热门帖子,我们将在子页面中添加两个文件:

子页面控制器...

subpage_controller.php

class subpage_controller 
    $post_model = new post_model();
    $popular_posts = $post_model->get_popular(5);

    $smarty->assign('posts', $popular_posts);
    $smarty->view('subpage.tpl');

...和一个子页面模板:

subpage.tpl

include file="header.tpl"

<div id="sidebar">

 foreach from='posts' item='post'
    <a href="post.php?id=$post.id">$post.title</a>
 /foreach

</div>

include file="footer.tpl"

新的子页面 controller 知道子页面应该只显示 5 个热门帖子。子页面 view 知道子页面应该将帖子列表放在侧边栏 div 中。

现在,我们要在主页上突出显示新帖子。我们可以通过修改homepage.tpl来实现。

include file="header.tpl"

 foreach from='posts' item='post'
    if $post.date_created == $smarty.now
        <a class="new-post" href="post.php?id=$post.id">$post.title</a>
    else
        <a href="post.php?id=$post.id">$post.title</a>
    /if
 /foreach

include file="footer.tpl"

视图在这里处理所有用于显示热门帖子的新逻辑。 controllermodel 不需要知道任何有关该更改的信息。这纯粹是显示逻辑。子页面列表继续像以前一样显示。

最后,我们想更改热门帖子的含义。我们希望它不是基于页面获得的点击次数,而是基于帖子获得的 cmets 数量。我们可以将该更改应用于模型:

post_model.php

class post_model 
    public function get_popular($number) 
        $sql = "SELECT * , COUNT(comments.id) as comment_count
                FROM posts 
                INNER JOIN comments ON comments.post_id = posts.id
                ORDER BY comment_count DESC 
                LIMIT $number";
        $result = mysql_query($sql);
        while($row = mysql_fetch_assoc($result)) 
            $array[] = $row;
        
        return $array;
     

我们增加了“热门帖子”逻辑的复杂性。但是,一旦我们在 模型 中进行了这种更改,新的逻辑就会在一个地方应用到所有地方。主页和子页面,在没有其他修改的情况下,现在将显示基于 cmets 的热门帖子。我们的设计师不需要参与其中。标记不受影响。

希望这提供了一个令人信服的示例,说明如何分离数据逻辑、应用程序逻辑和显示逻辑的关注点,可以使您的应用程序开发更加容易。一个领域的变化往往对其他领域的影响较小。

遵循这个约定并不是让您的代码自动变得完美的灵丹妙药。毫无疑问,你会遇到一些不太清楚应该在哪里分离的问题。最后,这一切都与管理应用程序中的复杂性有关。

您应该充分考虑如何构建模型。他们将提供什么样的接口(参见 Gregory 关于合同的回答)?控制器和视图期望使用什么数据格式?提前考虑这些事情会让事情变得更容易。

此外,在开始一个项目以使所有这些部分很好地协同工作时,可能会有一些开销。有许多框架为模型、控制器、模板引擎、url 路由等提供构建块。有关 PHP MVC 框架的建议,请参阅 SO 上的许多其他帖子。这些框架将使您启动并运行,但您作为开发人员负责管理复杂性并实施关注点分离。

我还要注意,上面的代码 sn-ps 只是简化的示例。他们可能(很可能)有错误。但是,它们在结构上与我在自己的项目中使用的代码非常相似。

【讨论】:

你赢了一天。多年来,我一直在寻找基于 PHP 示例的 MVC 解释。 这篇文章清晰地展示了 MVC 和关注点分离:andywardley.com/computers/web/mvc.html【参考方案3】:

谁能提出一个论点 [...] 来解释为什么 MVC 是一个好主意?

它可以帮助您记住代码的作用,因为它们彼此隔离,从而使您保持清醒。

【讨论】:

【参考方案4】:

我不知道任何关于 MVC 主题的好书,但根据我自己的经验。例如,在 Web 开发中,您经常与设计师合作,有时与 dbas 合作。将逻辑与演示分开可以让您更好地与具有不同技能的人一起工作,因为设计师不需要太多的编码,反之亦然。此外,对于 DRY 的概念,您可以使您的代码更少重复且更易于维护。您的代码将更具可重用性,并使您的工作更加轻松。它还将使您成为更好的开发人员,因为您将变得更有条理并以不同的方式思考编程。因此,即使您必须从事非 MVC 的工作,您也可能会采用不同的方法来构建项目,因为您了解 MVC 概念。

我猜想为大型网站使用许多 MVC 框架的权衡是它可能不够快来处理负载。

【讨论】:

我认为这是一个非常好的观点。我的一位同事提出了类似的答案;允许具有不同技能的人处理他们需要的应用程序。【参考方案5】:

这是在教授 OOP 方面的失败,使用诸如 rectangle.draw() 和恐龙.show() 之类的例子完全没有意义。

当您谈到拥有一个显示自身的用户类时,您几乎是在回答自己的问题。

“稍后,如果我需要调整显示他们的名字+小头像,我可以更新这个方法,嘿-presto,我的应用程序更新了。”

暂时只考虑那一小部分。现在看看 Stack Overflow 并注意您的用户名出现的所有地方。在每种情况下看起来都一样吗?不,在顶部,您的用户名旁边只有一个信封,然后是您的声誉和徽章。在问题线程中,您的头像后面是您的用户名,下面是您的声誉和徽章。您是否认为存在具有 getUserNameWithAvatarInFrontOfItAndReputationAndBadgesUnderneath() 等方法的用户对象?不。

一个对象关心它所代表的数据和作用于该数据的方法。您的用户对象可能具有 firstName 和 lastName 成员,以及检索这些片段所需的 getter。它也可能有一个方便的方法,比如 toString()(用 Java 术语),它将以通用格式返回用户的姓名,比如名字后跟一个空格,然后是姓氏。除此之外,用户对象不应该做太多其他事情。由客户决定它想对对象做什么。

以你给我们的用户对象为例,然后想想如果你在其中构建一个“UI”你会如何做:

    创建一个显示所有用户的 CSV 导出文件,按姓氏排序。例如。姓,名。 提供重量级 GUI 和基于 Web 的界面来处理用户对象。 在一个地方的用户名旁边显示一个头像,但在另一个地方只显示用户名。 提供用户的 RSS 列表。 在一处显示用户名粗体,在另一处显示为斜体,在另一处显示为超链接。 在适当的地方显示用户的中间名首字母。

如果您考虑这些要求,它们都归结为提供一个只关注它应该关注的数据的用户对象。它不应该试图对每个人都适用,它应该只是提供一种获取用户数据的方法。由您将创建的 许多 个视图中的每一个来决定它希望如何显示用户数据。

您关于在一个地方更新代码以在多个地方更新您的视图的想法是一个很好的想法。这仍然是可能的,而无需处理太低的水平。您当然可以创建类似小部件的类来封装各种常见的“东西”视图,并在整个视图代码中使用它们。

【讨论】:

好答案。我给 Allen Holub(Javaworld 文章作者)发了电子邮件,他警告说“提供”自己的 UI 的对象和“负责”自己的 UI 的对象之间是有区别的。与您提出的观点相似;我认为。 另一篇 Javaworld 文章的一个建议是使用 Builder 模式将 UI 从对象中分离出来,但仍然专注于松散耦合的系统:进一步阅读:javaworld.com/javaworld/jw-01-2004/jw-0102-toolbox.html【参考方案6】: 考虑代码量 会进入那个单一的班级,如果 您不想公开相同的信息 仅作为 UI 上的 Html,但作为一部分 一个 RSS,一个 JSON,一个休息服务 使用 XML,[插入其他内容]。 这是一个有漏洞的抽象,这意味着它试图让您感觉到它将是唯一知道该数据的部分,但这不可能完全是事实。假设您想提供一项与多个外部第三方集成的服务。您将很难强迫他们使用您的特定语言与您的服务集成(因为它是唯一可以使用它的数据的类),或者另一方面,如果您公开它的一些数据您没有向这些第三方系统隐藏数据。

更新 1: 我对整篇文章进行了整体查看,作为一篇旧文章 (99),它并不是真正关于我们今天所知道的 MVC 与面向对象的对比,也不是有反对 SRP 的论点。

您完全可以按照他所说的来处理我提到的上述场景,并使用负责将对象的公共合同转换为不同格式的特定类:主要问题是我们没有明确的位置来处理处理更改,并且我们不希望信息重复一遍。因此,在 html 案例中,您可以完美地拥有一个呈现信息的控件,或者一个将其转换为 html 的类或 [在此处插入重用机制]。

顺便说一句,我对 RMI 位有一个闪回。无论如何,在那个例子中,你可以看到他与一种交流机制有关。也就是说,每个方法调用都是远程处理的。我认为他也非常关心开发人员的代码,而不是获取单个对象并对返回的信息进行操作,而是通过大量小的 Get 调用来获取大量不同的信息。

附言。我建议您阅读有关 DDD 和 Solid 的信息,正如我在 SRP 中所说的那样,我不会说这是作者抱怨的事情的类型

【讨论】:

为什么我不能有两种方法; “display_yourself”和“display_yourself_as_text”或类似的东西?为什么我需要比这更多的方法?为什么有很多代码? @Dave 归结为要求,您可能需要将其公开为 Xml、JSon、Rss 等 - 采用多种不同格式。 @Dave 我在进一步查看文章后更新了我的答案......我会是作者对今天有些人如何使用 MVC 有不同的看法【参考方案7】:

我不确定我能否带你去喝你想喝的水,但我想我可以回答你的一些问题。

首先,在 MVC 中,模型和视图确实有一些相互作用,但视图确实与契约耦合,而不是与实现耦合。您可以转移到遵守相同合同的其他模型,并且仍然能够使用该视图。而且,如果你仔细想想,这是有道理的。用户有名字和姓氏。他可能还有一个登录名和密码,尽管您可能会将其与用户的“合同”联系起来,也可能不会。关键是,一旦您确定了用户是什么,就不太可能发生太大变化。您可能会向其中添加一些内容,但您不太可能会经常删除。

在视图中,您有指向遵守该合同的模型的指针,但我可以使用一个简单的对象:

 public class User
 
    public string FirstName;
    public string LastName;
 

是的,我意识到公共领域很糟糕。 :-) 我也可以使用 DataTable 作为模型,只要它公开 FirstName 和 LastName。这可能不是最好的例子,但关键是模型与视图无关。视图与合同相关联,并且特定模型遵守该合同。

我没有听说过每个对象都必须有自己的 UI。基本上有两种类型的对象:状态和行为。我见过同时具有状态和行为的示例,但它们通常位于不太可测试的系统中,我不喜欢这样。最终,每个状态对象都应该暴露给某种形式的 UI,以避免迫使 IT 人员直接在数据存储中处理所有更新,但有自己的 UI?我必须在解释中看到这一点,以尝试了解用户在做什么。

至于 SoC,将事物清晰封装的原因是能够在不重写整个系统的情况下切换层/层。一般来说,该应用程序确实位于业务层,因此该部分不能轻易切换。在设计良好的系统中,数据和 UI 应该相当容易切换。

就理解 OOP 的书籍而言,我倾向于喜欢有关模式的书籍,因为它们是理解概念的更实用的方法。您可以在网上找到底漆材料。如果您想要一本与语言无关的模式书,并且觉得有点怪异,那么“四人帮”书是一个不错的起点。对于更多创意类型,我会说 Heads Up Design Patterns。

【讨论】:

有趣的想法。我已经阅读了一些模式书(包括 GOF)。当人们谈论“切换层/层”时,我总是觉得很有趣。根据我的经验,这些很少是我想要对系统进行的更改。更有可能我想为“用户”添加功能。 @Dave tiers != 层,当您使用需要扩展的真正大型应用程序时,您通常最终会拥有跨不同服务器的进程【参考方案8】:

所有对象都知道如何显示自己的想法的问题是每个对象只能以一种方式显示。如果您想提供用户的详细视图和摘要视图,会发生什么情况。如果您想显示一个合并多个对象(例如用户及其关联地址)的视图,会发生什么情况。如果您将业务对象(用户)与知道如何显示它们的事物分开,那么您就无需编写更多代码,只需将其分隔到不同的位置即可。

这使得软件更易于维护,因为如果用户对象的行为不正确,您知道它是用户,如果它没有正确显示,您知道它是视图。在您需要为应用程序提供新界面的情况下(假设您决定为移动浏览器提供新的外观和感觉),那么您根本不需要更改用户对象,您可以添加一个知道如何操作的新对象为移动浏览器呈现用户对象。

SOLID 原则为此提供了一些很好的理由,here 是对这些的相对简洁的看法。恐怕我手头没有一本可以很好地总结它的书,但经验告诉我,编写新代码比更新旧代码更容易,因此设计倾向于将小型模块化类插入到实现所需的东西,虽然前期设计更难,但从长远来看更容易维护。能够为用户对象编写新的渲染器,而无需深入研究该对象的内部结构,这真是太好了。

【讨论】:

如何将渲染器连接到对象?渲染器是否需要“深入”到对象的内部?

以上是关于关注点分离; MVC;为啥?的主要内容,如果未能解决你的问题,请参考以下文章

asp.netMVC中使用aop进行关注点分离

ASP.NET 5/ASP.NET Core 1 中的关注点分离和 n 层架构

MVC思想概述

asp.net mvc项目搭建

MVC 优于 MVP 的优势 [重复]

MVC/MVVM模式特点及区别