MVC模型构建应用程序

6 使用MVC模型来构建应用程序

6.1 为什么应用程序结构很重要?

保护应用程序需要我们实施大量检查和组件,每个检查和组件都有其特定的作用。每项检查或组件涉及应用程序的不同方面:验证发送到服务器的数据、日志记录和权限管理、保护数据库、对发送到浏览器的数据进行编码以及加密连接等。

二十年前,安全只需两三个功能,相对容易从一个页面复制到另一个页面。如今,必须实施的机制的复杂性使得这种方法已经过时:必须将保护功能集中在应用程序的相关部分中。

同样,早期软件中的代码相对简单。计算机通常具有相同的屏幕尺寸,处理能力较慢,互联网连接速度也有限(下载超过200 KB的页面会出现问题)。程序员仅编写严格必要的脚本,而面向对象编程由于会带来性能损失而受到批评。

如今,每个现代PHP应用程序都包含数千行代码,并分布在日益复杂的文件系统中。“逐页”开发的方式已不再可行:唯一的解决方案是采用高层级的一种能够提供应用程序结构和运行全局概览的架构。

已经提出了几种用于组织此类应用程序代码的方法。其中最流行的是MVC模型,它已被进一步发展为多种变体,以满足特定的方法或需求。

6.2 什么是MVC模型?

MVC模型或模式(模型、视图、控制器)是一种开发结构,用于描述应用程序内三种不同类型的行為。

模型层实现所有的应用逻辑,即软件为实现其功能必须执行的操作序列。视图层显示应用程序生成的所有输出。在大多数情况下,这些输出以发送到浏览器的网页形式呈现,但也可以是PDF文档、JSON或XML格式数据等。最后,控制器负责协调整个应用程序:它管理应用程序内的导航以及处理模块的调用。

浏览器向应用程序发送请求。控制器拦截此请求。在执行所请求的模块(请求)之前,控制器检查用户是否具有必要的权限。

一旦完成这些检查,模块就会被执行。它可能需要从数据库中检索信息:该模块可以直接查询数据库,或者使用一个充当接口的类。这两个模块,即模块和数据库访问,构成了MVC模式中的模型。

执行期间,模块将信息发送到视图,由视图负责显示。一旦模块执行完成,便将控制权交还给控制器。根据应用逻辑,控制器可能会执行另一个模块。这通常是更新操作的情况:一旦信息被记录到数据库中,控制器通常会要求显示列表,或更详细地显示已处理的信息。当所有操作完成后,控制器将要求视图开始显示(或发送信息到浏览器)。

示意图0

6.2.1 模型

模型包含应用逻辑。它通常由两个独立的部分组成:第一部分管理与数据库的交互,第二部分包含特定于应用程序的代码。

在此级别实施了多项安全措施。负责向表发送查询的子模型必须实施保护数据库所需的所有措施,例如防止SQL代码注入的检查(参见第4.2.1节)。在大多数情况下,会使用特定的类,有时称为对象关系映射(ORM)[WIK 16b],它们将数据库结构(表)转换为应用程序直接操作的类。这些ORM通常包含常规安全检查,例如防范SQL注入的措施。

另一方面,代码模块负责数据库查询前后发生的所有事情,例如前面讨论的转码密钥(参见第4.2.4节)。

该模型使用赋值命令向视图提供信息,例如:

$view->set($data, 'variableName');

它不会修改显示本身:仅发送信息。视图则负责将这些信息组织成预期的格式(网页、JSON 或 XML 文件等)。

6.2.2 视图

在MVC模型中,视图是发送给用户的应用程序部分。这通常是用于查看或输入信息的接口。在Web应用程序中,视图由HTML页面及其所有组件构成:包括HTML代码,当然也包括决定信息呈现方式的样式表,以及使网页具有动态性并允许在用户浏览器中直接执行计算的JavaScript代码。

通常,网页是使用模板引擎生成的。这些引擎会向每个页面的HTML代码中添加特殊代码,以集成模型发送的数据。Smarty [SMA 16] 就是一个例子,它提供了一个类,能够根据传输的数据实时生成网页。在此示例中,视图使用Smarty生成发送到浏览器的代码。

应用程序很少只发送网页。AJAX请求允许在不重新加载整个页面的情况下从服务器动态获取信息,其基于JSON格式的文件传输。

CSV格式通常用于将数据导出到电子表格。在将文件发送到浏览器之前,我们必须准备一个特殊的HTML头部,以声明MIME类型¹。

尽管在MVC模型中视图被视为一个单独的组件,但实际上存在多种类型的视图,用于发送不同的格式。图6.2展示了一个视图类层次结构的示例。

¹ MIME类型(互联网媒体类型)是一种标准[WIK 16c],它允许我们向浏览器指定信息的格式。浏览器随后可以选择适当的处理方式来处理这些信息。例如,具有text/csv MIME类型的文件可以被下载或使用电子表格软件打开,而PDF文件会以application/PDF MIME类型发送,以便可以通过PDF阅读器打开。

在图6.2中,定义了三个类,每个类都继承自一个从不直接实例化的初始类。以下展示了一个示例实现。主控器类View包含三个功能,其中一个专门用于安全:

class View {
    /**
     * 要发送的数据(非HTML情况)
     * @var 数组
     */
    protected $data = array();

    /**
     * 赋值
     * @param 未知 $value
     * @param 字符串 $variable
     */
    function set($value, $variable = "") {
        $this->data = $value;
    }

    /**
     * 开始显示
     * @param string $param
     */
    function send ($ $param = "") {
    }

    /**
     * 递归HTML编码函数
     * 用于变量
     * 
     * @param string |array $data
     * @return string
     */
    function encodehtml ( $data ) {
        if ( is_array ( $data ) ) {
            foreach ( $data as $key => $value ) {
                $data [$key ] = $this -> encodehtml ( $value );
            }
        } else {
            $data = htmlspecialchars ( $data );
        }
        return $data;
    }
}

set()函数允许我们分配数据。除非将单个变量传递给视图(例如,包含所有要格式化为JSON的数据的数组),否则派生类需要重载此参数。对于send()函数也是如此,该函数启动将信息发送到浏览器的过程。

encodehtml()函数是我们之前用于防范XSS攻击的递归函数(参见第4.2.3节):它在将<、>、&和引号等字符发送到浏览器之前对其进行编码,以便由浏览器的HTML解释器处理。

6.2.3 控制器

控制器起着至关重要的作用:它决定所请求的模块是否可以执行。
它必须验证用户是否有正确的权限来执行所请求的操作。
它还负责清理作为输入提供的数据(例如检查 UTF‐8 编码——参见第 4.3.1 节)。根据执行模块后获得的结果,如有必要,它将调用其他模块。例如,
在一个模块执行写操作后,控制器随后会调用另一个模块以在浏览器中显示新页面。

最后,一旦所有操作完成,它会请求视图启动将信息发送到浏览器的过程。所有这些任务可以总结如图6.3所示。

控制器首先执行各种安全检查,例如验证UTF‐8编码、检查IP地址等。如果请求的模块

要求用户登录,但如果尚未执行此操作,则会将用户重定向到登录模块。
执行模块可能需要特定的权限,这些权限将由控制器进行验证。此外,可能还需要进行故事板检查,以确保仅在显示输入表单后才将记录写入数据库(图中未显示)。

执行模块后,控制器可以根据返回的代码选择执行其他模块。正如我们所见,这在处理表单时很常见:应用程序随后需要显示结果或返回到文件夹列表,而此显示过程通常由专用模块描述。

最后,脚本要求视图开始显示。

6.2.3.1 关联控制器

我们考虑了一个相对简单的示例(但对于大多数Web应用程序来说已足够),即一个应用程序仅包含一个控制器来处理所有事务。

在其他语言中,例如 Java,存在用于操作与其相关对象的专用控制器。它们可以通过观察机制相互通信(控制器会向其他控制器注册,每当执行某个操作时,每个控制器都会通知已注册控制器列表)。

必须确保控制器在任何时刻仅被另一个控制器监听,遵循如图6.4所示的树状结构。我们应避免在各个方向上建立连接(有时被称为意大利面条式代码)。这将保证我们能够长期维护代码,因为其路径相对容易追溯。

这种方法通常由用 Java 编写的“客户端较重”的应用程序实现。
主控制器从专用控制器接收信息,通常每个输入模块对应一个控制器(这些模块可以选择性地在独立窗口中声明)。窗口中的每个组件,例如菜单,也可以由其自己的控制器进行操作。

如果一个窗口中的信息需要传递到另一个窗口,则整个控制器链负责传输该消息:这确保了消息仅被处理一次,并提高了代码的可重用性。
添加或删除窗口不会破坏整体架构。

6.2.3.2 继承的控制器

大多数专业框架使用继承的控制器。为了执行一个模块,会从包含基本代码的单个类创建一个控制器。为了让该控制器可用,我们只需指定分配给它的权限、它生成的视图等。

事实上,Symphony(专业领域广泛使用的PHP编程框架 [SYM 15])采用的就是这一原理。控制器、模型的一部分以及视图的一部分(如上所述)被合并在一起:每个可执行的模块和页面都有一个对应的控制器。

所有这些控制器都继承自一个底层对象,该对象已包含管理权限、登录和操作序列(执行后控制权移交给另一个控制器)所需的机制。这种方法的主要缺点是难以快速可视化应用程序的整体架构,并且修改其结构或权限管理可能更加困难:这些方面是在每个控制器内部定义的,而不是在单一位置定义的。

6.2.3.3 单对象控制器

在处理高强度图形处理的环境中,例如绘图软件,一些控制器会实时监控对单个图形组件所做的修改。每个图形对象都关联一个控制器,每当执行一个操作时,该控制器就会被激活。此操作随后会被传播到所有其他监听控制器,从而触发与这些监听控制器之一相关联的所有对象的状态变化。

因此,修改表单中的任何字段将立即提示控制器检测新的输入值,并将其发送到数据库管理处理器(ORM)。

这种方法比通常在Web环境中使用的方法更具反应性,在Web环境中,信息只有在用户验证后才会被考虑,例如通过单击“提交”按钮。

然而,如果某些操作需要知道多个其他组件的状态,则可能更难以实现。

6.3 结论

我们已经看到,MVC模型通过确保所有信息都得到一致地处理,使得整个应用程序代码更易于保护。

对于初级开发者来说,使用MVC模型可能会有些不安。事实上,在显示任何信息以及测试任何操作之前,需要在整个模型的各个部分(视图、模型管理类和控制器)中编写大量适当的代码行。这需要一定程度的抽象能力,而在同时学习编程实践和其他编程语言相关知识时,这种抽象能力可能难以达到。

尽管编写第一个模块可能是一个繁琐的过程,但学习MVC模型所投入的精力将会带来丰厚的回报,因为它使得后续模块更易于编写和维护,并提供对应用程序的全局概览。

从安全角度来看,使用这种设计方法(或其他等效方法)实际上是强制性的,以确保在整个开发过程以及应用程序的连续版本中保持一致的安全水平。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值