更多关于三层设计
上个月,我们开始了对 Web 应用程序的三层设计的调查。通过使用“中间件”对象层将数据库服务器与 Web 应用程序本身分离,我们简化了 Web 应用程序中的逻辑。此外,通过在 Web 应用程序层和数据库层之间添加抽象层,我们获得了在非 Web 应用程序中使用相同中间件的能力,以及在不告知 Web 应用程序的情况下更改后端的可能性。
在上个月的专栏结束时,我们已经实现了一个简单的中间件层,可以与我们在 PostgreSQL 数据库中创建的 People 和 Appointments 表进行通信。本月,我们将简要了解一些可以使用这些对象开发的 Web 应用程序。您将看到,我们的 Web 层在任何时候都不会直接访问关系数据库;SQL 全部包含在对象中。
在理想的世界中,我们可以使用我们想要的任何语言或技术来创建 Web 应用程序层,并使用普遍认可的协议与中间件层进行通信。然而,世界并没有我们想象的那么先进,我们在选择对象层时,也限制了我们在选择 Web 应用程序环境时的自由。
我们在 Perl 中创建了对象,因此我们需要使用 Perl 来实现我们的 Web 应用程序。为了避免与 CGI 程序相关的开销,并且因为我们可以通过利用 Apache 的 mod_perl 模块获得更大的 power,我们将使用 Mason,这是一个基于 Perl 的模板和开发应用程序环境,我们在去年对其进行了研究。每个 Mason 组件都根据需要编译成 Perl 子例程,然后编译成 Perl 操作码。这些操作码随后缓存在 Apache 内部的 mod_perl 模块中,在那里它们的执行速度比使用 CGI 可能的速度快得多。
我们的第一个 Web 应用程序示例将允许我们向数据库添加新人员。这将需要两个 Mason 组件:一个 HTML 表单(同样可以是静态表单)和一个尝试向数据库添加新人员的组件。为了实现这一点,我们将使用中间件 People 对象,它为我们连接到数据库并尝试在数据库中存储新行。列表 1 和 2 中显示了这两个组件的简单版本。这些列表太长,无法在此处打印;它们可在 ftp.linuxjournal.com/pub/lj/listings/issue82 中找到。HTML 表单 (add-person-form.html) 将其名称-值对提交给 add-person.html。后者创建一个 People 实例,然后调用 new_person 方法来创建新人员
my $success = $people->new_person (first_name => $first_name, last_name => $last_name, country => $country, email => $email);
如果 $success 为真,我们知道已使用传递给 $people->new_person 的参数将新人员添加到数据库中。否则,我们知道调用失败了。
然而,这是一种非常粗略的确定事情是否成功或失败的方法;与其向用户呈现一个全有或全无的命题,不如告诉他们哪里做错了,以便他们可以解决问题。如果挂起的数据库进程产生的错误消息与尝试添加具有相同电子邮件地址的第二个人产生的错误消息相同,那么任何人都不容易解决问题。
因此,解决方案是让我们的 Web 应用程序在将输入传递到中间件层之前检查其输入。我们在代码中插入的此类检查越多,我们可以显示的应用程序级错误消息越多,效果就越好。
我们的 add-person.html 组件执行两个基本检查来证明这一点:它使用 Mason 的 <%args> 部分来要求已传递每个潜在参数。尝试将其值提交到 add-person.html 的 HTML 表单必须提供列出的每个表单元素,否则 Mason 将拒绝接受该请求并打印描述发生错误的堆栈跟踪。最终用户在填写表单时犯错不会看到此错误,但如果您遗漏了必需的 <input> 标记,您将会看到它。
一旦我们的 Mason 组件执行,我们就可以确定我们至少已收到适当的名称-值对。但是它们是否包含合法值?在 add-person.html 顶部的“unless”语句中,我们检查我们是否收到了我们将用于调用 $people->new_person 的四个参数的非空值。如果缺少任何一个参数,则会显示一条消息,告知用户需要什么。
为了更安全,我们还检查电子邮件地址看起来是否相对有效。列表 2 中的正则表达式不会匹配所有电子邮件地址,但对于此简单示例的目的而言,它已足够好。尝试传递无效电子邮件地址的用户将看到一条错误消息,告知他们需要更改什么。
一旦我们可以确定这些值相对合理,我们就可以调用 $people->new_person。请注意 add-person.html 如何在从不直接与数据库通信的情况下完成所有这些操作。DBI 显然在每次调用 $people->new_person 时都发挥着积极作用,但这发生在幕后,我们的 Mason 组件无需关心它。这意味着如果 People 对象已彻底调试,则不应有遇到 SQL 错误的机会。
现在我们已经了解了如何使用 People 对象向数据库添加新人员,让我们尝试一个稍微困难的任务:使用 update_first_name 方法更改人员的名字。(有关示例,请参见 ftp://ftp.linuxjournal.com/pub/lj/listings/issue82/ 中的列表 3 和列表 4。)我们只能在选择个人后才能调用此方法,这意味着我们的编辑表单必须允许我们这样做。
虽然让用户通过在文本字段中键入姓名或电子邮件地址来选择条目可能很诱人,但这很容易出错,效果不佳。相反,我们将允许用户从 <select> 列表中进行选择。这消除了用户为可能不在我们数据库中的人员输入电子邮件地址(或其他定义特征)的可能性。
我们希望使用唯一键来选择要修改其名字的人员,但与此同时,显示电子邮件地址列表似乎有点不人性化。我的解决方案是回到 People 对象 (People.pm) 并定义一个新方法 get_names_and_addresses(请参见 ftp://ftp.linuxjournal.com/pub/lj/listings/issue82/ 中的列表 5)。这会返回数组引用列表,其中每个数组引用都包含一个姓名和一个电子邮件地址。前者可以用作唯一键(以及 <option> 标记中的“value”),而后者可以用于显示目的。因此,我们可以迭代电子邮件地址并生成一个 <select> 列表,如下所示
<select name="email"> % # Iterate through the names and addresses, # printing them out % foreach my $info (@names_and_addresses) { <option value ="<% $info->[1] %>"><% $info->[0] %> % } </select>
允许用户编辑其他用户属性将以类似的方式进行。实际上,只要您确保用户选择的键能够唯一标识用户,您就可以使用类似类型的表单更改其任何和所有属性。
现在我们已经了解了如何使用 People 对象间接操作数据库中的 People 表,我们将开始研究我们的预约簿,由 Appointments 对象处理。此对象允许我们在特定的日期和时间与特定人员添加预约。
为了实现这一点,我们将(再次)需要两个组件。第一个组件 (add-appointment-form.html,在 ftp.linuxjournal.com/pub/lj/listings/issue82 中的列表 6 中) 生成一个 HTML 表单,允许用户向系统中输入新预约,并从预定义的 <select> 列表中选择人员。(如果这是一个实际项目,我会将 <select> 列表放在一个单独的组件中,允许其他组件在地址簿中生成条目菜单。)此外,我们必须知道预约的开始时间和结束时间。再次,我更喜欢让人们从 <select> 列表中选择日期和时间,因为它消除了与时间和日期格式相关的问题。
以下 Mason 代码生成了我们需要用来让用户选择月份、日期和年份的三个 <select> 列表。通过预先定义 @months 和 @years 数组,我们可以使代码更具可读性,并快速轻松地为未来几年更新系统
<select name="begin_month"> % foreach my $month (@months) { <option value="<% $month %>"><% $month %> % } </select> <select name="begin_day"> % foreach my $day (1 .. 31) { <option value="<% $day %>"><% $day %> % } </select> , <select name="begin_year"> % foreach my $year (@years) { <option value="<% $year %>"><% $year %> % } </select>
第二个组件 add-appointment.html(请参见 ftp.linuxjournal.com/pub/lj/listings/issue82 中的列表 7)允许我们向预约日历中添加新条目。它检查(使用 <%args> 部分)我们是否已提交 add-appointment-form.html 中的所有必需的名称-值字段。然后,我们发出与其他组件相同的基本检查。
现在我们已经演示了创建三层 Web 应用程序有多么容易,现在是时候考虑我们真正使用了多少层了。“三层”这个术语真的适用于这里吗?
“三层架构”一词源于对另一种流行的架构“客户端/服务器”的不满。例如,数据库和 Web 服务器都是现代客户端/服务器系统的示例。正如客户端/服务器系统通常指的是两台物理计算机一样,三层系统指的是三台物理计算机,每层都位于单独的机器上。
相比之下,我们检查的简单三层应用程序确实有三层,因为存在具有明确目标和 API 的不同软件系统,并且使应用程序层和数据库层可以通过公共中间件层进行通信。与此同时,这些层中至少有两层(Web 应用程序和中间件对象)在同一台计算机上,没有任何真正的分离可能性。如果 Web 应用程序被流量淹没,我们当然可以添加一个或多个相同的 Apache 服务器,但是无法将应用程序层放在一台计算机上,而将中间件对象放在另一台计算机上。
因此,虽然我认为我们现在已经从需要标准 API 的应用程序开发人员的角度展示了三层架构的一些优势,但我们还没有看到这种系统的真正实现。但是,为了做到这一点,我们必须具有执行远程过程调用 (RPC) 的能力,以便一台计算机上的 Web 应用程序可以调用另一台计算机上的子例程或对象方法。这有可能实现,并且随着 SOAP(简单对象访问协议)的增长,实现起来也越来越容易,但这带来了一些其他问题和注意事项,包括需要学习另一种传输协议。
当我们重新考虑如何定义层时,也许我们应该考虑开发的应用程序确实有三层,但它们的定义方式与我们之前考虑的不同。与其将层数计算为 (a) 数据库、(b) 中间件层和 (c) Web 服务器,不如将它们计算为 (a) 数据库、(b) Web 服务器和 (c) Web 浏览器?如果我们从这些角度考虑,那么我们确实创建了一个三层架构,但仔细想想,任何编写过与数据库对话的 CGI 程序的人也是如此。
此外,我们可以在这里引入额外的抽象层,使事情进一步复杂化。在关系数据库上创建的存储过程、触发器和视图呢?虽然不是物理层,但它肯定可以使编写对象层或访问数据库的应用程序的人员的生活更轻松。实际上,存储过程通常比对象中间件层更好,因为它们在数据库上执行并且是预编译的,这使得它们相对快速。
我们还可以使用 JavaScript 在 Web 客户端(即 Web 浏览器内部)执行代码。虽然我通常鼓励我的客户尽可能避免使用 JavaScript,但这种充满错误、不安全且存在跨平台不兼容问题的语言是从 Web 浏览器而不是服务器内部执行程序的唯一方法。
因此,当我们有一个 Web 应用程序,它使用关系数据库、存储过程、对象中间件层、Web 应用程序层和客户端 JavaScript 分布在三台计算机之间时,我们有多少层?可能仍然是三层,但事实是称呼它什么并不重要。最后,一个体面的设计,考虑到您的项目规范,包括未来增长的需求,才是正确的方向,无论它与最新的流行语和技术是否一致。
现在我们已经研究了一个非常简单的三层项目,现在是时候研究与这种设计相关的一些问题了。我并不是说三层解决方案本身是邪恶的,但它们也不是万能药。像大多数解决方案一样,它们在某些情况下是合适的。在许多情况下,当您将工作划分为不同的层时,在几个人之间拆分设计和实现可能会更容易,正如我们在基于 Web 的预约日历中看到的那样。一个人可以编写所有必要的代码,但如果他们之间有良好记录的接口,两个人可能会更快更轻松地完成。
与任何工程解决方案一样,总是有权衡取舍。对于三层设计,也许最重要的权衡是时间。即使最终它会更健壮、更易于编写、更易于测试并且更易于在许多程序员之间分配,但这种架构也需要更长的时间来指定和设计。
虽然将项目划分为许多部分可能会使指定和测试每个部分更容易,但它使集成测试变得更加重要和困难。如果每个人都坚持已发布和商定的 API,则此类测试不必非常困难。但是,规范和实现之间总是存在差异,集成测试往往会揭示这些差异。项目中的层数越多,此类测试就越重要和困难。
最后,创建提供数据库接口的对象中间件层可能既困难又令人沮丧。SQL 不是一种完美的语言,但它允许我们用非常少量的命令来表达大量的查询。
从 Mason 组件中删除 SQL 并迫使程序员使用对象 API,意味着程序员将被限制在数据库的功能和灵活性的一小部分子集中。每次需要更多功能时,程序员都必须请求将其添加到中间件的 API 中。对于程序员来说,能够在 HTML 模板(例如 Mason 组件)内部指定任何数据库查询是一种解放体验,而剥夺这种自由可能会令人沮丧。
