03 Rails 应用程序结构
Rails 有一个有趣的特点,它会对你的 web 应用结构增加限制。不过,这些限制使它更加容易创建应用。让我们看看其中的原因。
Models, Views 和 Controllers
回到 1979 年,Trygve Reenskaug 提出了开发交互应用的一个新架构。在他的设计中,应用分离为三种类型的组件:model,view 和 controller。
model 负责维护应用的状态。有时状态是瞬态的,就只存在于用户的交互中。有时,状态是持久的并被存储在应用外部,通常是数据库中。
但一个 model 并不止是数据,它也执行应用于数据的业务规则。例如,如果订单少于 20 美元时不进行折扣,那么 model 应该执行这个限制。也就是说,业务规则的实现应该放入 model 中,我们需要确认应用中没有任何事情会导致数据失效。所以 model 的行为像一个守门员和数据商店。
view 负责生成用户接口,通常是基于 model 中的数据。例如,线上商店应该有一个商品列表用于分类场景中进行展示。这个列表通过 model 访问,但它最终是根据用户需要格式化后的 view。尽管 view 会根据用户不同的输入数据显示,但 view 本身并不处理输入的数据。view 的工作只是展示数据。所有通常因为不同的目的,是有可能通过多个 view 访问相同的 model 数据的。在线上商店,需要一个 view 在目录页展示商品信息,还需要其他 view 由管理员操作,用于对商品的添加和编辑。
controller 编排整个应用。controller 获取外界的动作(通常是用户输入),然后与 model 交互,最后向用户显示合适的 view。
model,view 和 controller 这三个组件共同组成了称作 MVC 的架构。
MVC 架构最开始是 GUI 应用的协议,开发人员发现分离关注点可以减少耦合,最后使代码的编写和维护更加容易。每个思想和行为都在一个著名的地方表现过。使用 MVC 就像是已经建行好桥梁的摩天大楼,它使你可以在当前的结构中轻松地添加空缺的部分。在我们应用的开发阶段,我们会大量使用 Rails 的能力搭建应用的脚手架。
Ruby on Rails 也是一个 MVC 构架。Rails 为你的应用生成一个结构,你负责开发 model,view 和 controller 作为分离的基础功能块,然后由 Rails 将它们纺织起来作为程序执行。Rails 其中让人开心的一点是编织的过程是通过一个默认的集成过程,这样就通常不需要再编写额外的配置元数据。这是一个关于 Rails 哲学——约定大于配置的例子。
在 Rails 应用中,输入的请求首先会传送给路由器,路由器运行于应用中并会将请求解析。最终,请求会定位到 controller 中的指定方法(在 Rails 中也叫做 action)。action 可以获取请求中的数据,它可以与 model 交互,也可以调用其他 action。接着 action 会为 view 准备好信息,view 将信息展示给用户。
Rails 如下图一样处理请求。在这个例子中,应用之前已经显示了产品分类页面,用户点击【添加至购物车】按钮添加了一个商品。这个按钮发起 http://localhost:3000/line_items?product_id=2,其中的 line_items
是应用中的资源,2 是所选端口的 ID。
路由组件获取输入的请求并立即拆解它。请求包含了路径(/line_items?product_id=2)和一个方法(按钮是使用 POST 请求,其他常用的还有 GET,PUT,PATCH 和 DELETE)。在常见例子中,Rails 获取第一部分的路径——line_items
并定位到相同名称的 controller 以及将 product_id
作为端口的 ID 处理。按照约定,POST 方法默认与 create()
action 关联。通过完整的分析,路由器知道它应该调用 controller 类 LineItemsController
中的 create()
方法(我们将在 270 页的 Section 18.2, Naming Conventions 讨论命名规范)。
create()
方法处理用户请求。在这个例子中,它会查找用户的购物车(由 model 对象管理)。它也会让 model 查找商品 2 的信息。并让购物车将产品添加进去。(想了解 model 怎样使用并追踪所有的业务数据吗?controller 会告诉它做什么,model 就知道怎么做了。)
现在购物车已经包含了新商品,我们可以向用户展示它。controller 调用 view 代码,但在此之前,controller 需要分配资源让 view 可以从 model 中访问购物车对象。在 Rails 中,调用是隐性的,再说一次,约定会帮助一个 action 链接到指定的 view。
这些都是 MVC 的 web 应用。下面还有一些约定会适当地参与到基础功能中,你会发现你的代码变得更加容易动作,应用也更加容易扩展和维护。这简直就是双赢。
如果 MVC 是参与人编码的一种方式,你也许会思考为什么还需要像 Ruby on Rails 这样的框架。答案是显而易见的,Rails 处理低级的家务活,而杂乱的细节部分就需要你来处理,并让你只关心你的应用核心方法。让我们看看怎样做。
Rails 对 Model 的支持
一般情况下,我们希望自己的 web 应用可以将信息保存在关系型数据库中。订单系统会存储定单,商品条目和用户信息在数据库表中。即使有些应用通常不使用结构化文本比如网站日志和新闻网站,但它们也使用数据库作为后端数据存储。
尽管 model 不可能立即通过你使用的 SQL 就显示出来,但关系型数据库是根据数学集合理论设计出来的。尽管从 view 出发这是一个很好的概念,但它使面向对象语言要结合关系型数据库变得困难。对象包含数据和操作,但数据库只是数据值的集合。表示关系型事物的操作变得简单了,但在面向对象系统中编码变得困难了。反过来也是一样。
随着时间的发展,人们通过一些方式调和了业务数据的关系型视图和面向对象视图。让我们看看 Rails 选择的在对象中匹配关系型数据的方式。
对象关系映射
ORM 库可以映射数据库表和类。如果数据库有叫做 orders 的表,我们的程序将有一个类叫做 Order。表中的每条数据与类的对象是一致的,指定的订单以 Order 类的一个对象表示。在对象中,元素被用来获取和设置虚拟的列。我们的 Order 对象有方法可以获取和设置总量,销售税等等。
而且,包装数据库的类还提供了一组类级别的方法,供我们执行表级别的操作。例如,我们可以需要通过指定的 ID 查找订单。作为类方法实现它需要返回相应的 Order 对象。在 Ruby 代码中,看起来可以是这样的:
order = Order.find(1)
puts "Customer #{order.customer_id}, amout=$#{order.amout}"
有时类级别的方法返回对象集合。
Order.where(name: 'dave').each do |order|
puts order.amount
end
最后,对应表中相应行的对象也拥有操作行的方法。也许最常用的是 save()
方法,它可以将一行数据保存至数据库。
Order.where(name: 'dave').each do |order|
order.pay_type = "Purchase order"
order.save
end
因此,一个 ORM 层级将表与类映射,行映射对象,列映射对象的元素。类方法可以进行表级别操作,实例方法可以对虚拟行进行操作。
在典型的 ORM 库中,你提供配置的数据在数据库实体和程序实体间进行匹配。程序员通过 ORM 工具查找并进行创建和维护这些 XML 配置文件。
动态记录
动态记录是由 Rails 的 ORM 层级提供。它与下面的 ORM model 是相似的,表与类映射,行与对象映射,列与对象属性映射。与 ORM 库中大多数情况不同,它是需要配置的。通过依靠约定和以合理的默认配置开始,动态记录最小化了开发者需要操作的配置。
为了阐明这个问题,这有一个程序通过使用动态记录包装 orders 表:
require 'active_record'
class Order <ActiveRecord::Base
end
order = Order.find(1)
order.pay_type = "Purchase order"
order.save
代码通过新的 Order 类获取 id 为 1 的订单并且修改了 pay_type。(我们当前忽略掉创建数据库连接的代码)。动态记录降低了我们大量处理数据库底层的工作,使我们可以专注于业务逻辑。
但是动态记录做的不止这些。但当我们从 55 页 Chapter 5, The Depot Application 开始开发购物车应用时,动态记录还是无缝地与 Rails 框架集成在一起。如果一个 web 表单传送应用数据给相关的业务对象,动态记录能够将其提取至 model 中。动态记录还支持复杂的 model 数据验证,如果表单数据验证失败,Rails 会显示准确且格式化的错误。
动态记录是 Rails MVC 架构的固态 model 的基础。
Action Pack:View 和 Controller
当你开始思考 view 和 controller 的时候,其实 view 和 controller 作为 MVC 的一部分关系是十分紧密的。controller 向 view 提供数据,以及 controller 从 view 生成的页面获取事件。因为这些互相作用,在 Rails 中是由 Action Pack 单一组件对 controller 和 view 一并支持的。
不要因为 Action Pack 是单一组件就愚蠢地认为应用中 view 代码和 controller 代码是纠缠在一起的。恰恰相反,Rails 对你需要编写的应用进行了分离,有清晰的控制分界代码和展现逻辑。
对 View 的支持
在 Rails 中,view 负责创建所有或者部分响应在浏览器中的展示,可以是作为应用的流程也可以作为邮件被发送。最简单的理解可以是,一个 view 就是一块 HTML 代码负责展现处理过的字符。通过情况下,你希望包含由 controller 中的 action 方法创建的动态内容。
在 Rails 中,动态内容由来自三种风格的模板生成。最常用的模板组合被称作嵌入式 Ruby(ERB),可以在 view 文档中嵌入 Ruby 代码片段,在其他 web 框架中也有许多相似的方法,比如 PHP 或 JSP。尽管这种方法非常灵活,但有些人认为它玷污了 MVC 的精神。在 view 中嵌入代码,我们就可能将需要在 controller 或 model 中处理的抛出给 view 处理。就如同所有的事情一样,适当地使用头脑是有益的,但过度使用就会出现问题。维护清晰的关注点分离是开发者日常工作的一部分。(我们可以在 142 页 Section 11.2, Iteration F2: Creating an Ajax-Based Cart 详细讨论)。
Rails 也提供 XML Builder 使我们可以通过 Ruby 代码构建 XML 文档,生成的 XML 结构将自动跟随代码结构。我们在 393 页 Section 24.1, Generating XML with Builder 讨论 xml.builder 模板。
还有 Controller!
Rails 的 controller 是应用的逻辑中心。它协调着用户,view 和 model 间的相互协作。然而,Rails 已经在幕后处理了许多相互协作的过程,你编写的代码主要关注应用级别的功能。这使得 Rails 的 controller 在开发和维护时都格外容易。
controller 也是许多从属服务的归属地。
- 它负责路由外部请求至内容 action。它需要非常好地处理用户友好的 URL。
- 它管理缓存,能够使应用的性能有大量的提升。
- 它管理辅助模块,可以扩展视图模板的代码而不增加它们的代码。
- 它管理会话,给用户正在与我们的应用正在交互的效果。
我们已经在 17 页 Section 2.2, Hello, Rails! 看到和修改了一个 controller,并且在开发一个简单应用的过程中我们还会查看和修改许多 controller,我们将在 91 页 Section 8.1, Iteration C1: Creating the Catalog Listing 开始处理商品 controller。
这里讨论了很多关于 Rails 的知识。但是在我们继续之前,还是让我们给大家对 Ruby 语言进行一个简单的介绍。