BT

如何利用碎片时间提升技术认知与能力? 点击获取答案

书摘:Java开发者的Rails之路

| 作者 Stuart Halloway 关注 3 他的粉丝 ,译者 马家宽 关注 0 他的粉丝 发布于 2007年12月26日. 估计阅读时间: 35 分钟 | QCon上海2018 关注大数据平台技术选型、搭建、系统迁移和优化的经验。

Ruby和Rails为web开发带来了许多优秀思想。关于Ruby和Rails的书已经不少,但Rails for Java Developers却别具一格。不同于“一切从零开始”的传统方式,我们在整本书中都是基于Java开发者的已有知识来展开讲述。 

将本书建构在Java知识之上可以使我们的讲述以比较的方式进行。贯穿全书始终,我们都将示例的Rails版本和Java web框架版本相比较,以便您能够迅速的了解其中的相似性和差异性。

举例来说,让我们来考察一个控制器方法,它负责保存业务逻辑中的某一模型对象。在这方面,大多数Java框架和Rails最大的差别在于:大多数Java框架中,每一个控制器都会显式的暴露大多数框架提供的编程模型(比如request、response)。而与之相反,Rails中对于绝大部分框架编程模型的使用是以隐式的方式进行。在Rails中,除非你确实需要xx,您才应该在程序中看到它

书摘1:控制器的保存方法

这里给出一个在Struts中保存或更新person模型实例的action:

code / appfuse_people / src/ web/ com/ relevancellc/ people/ webapp/ action/ PersonAction.java
public ActionForward save(ActionMapping mapping, ActionForm form,
HttpServletRequest request,
HttpServletResponse response)
throws Exception {
ActionMessages messages =new ActionMessages();
PersonForm personForm = (PersonForm) form;
boolean isNew = ("".equals(personForm.getId()));
PersonManager mgr = (PersonManager) getBean("personManager");
Person person = (Person) convert(personForm);
mgr.savePerson(person);
if(isNew) {
messages.add(ActionMessages.GLOBAL_MESSAGE,
new ActionMessage("person.added"));
saveMessages(request.getSession(), messages);
return mapping.findForward("mainMenu");
} else {
messages.add(ActionMessages.GLOBAL_MESSAGE,
new ActionMessage("person.updated"));
saveMessages(request, messages);
return mapping.findForward("viewPeople");
}
}

让我们从用户编辑成功这个令人愉快的场景开始思考。这段代码的大多数内容与前面的例子相似;新引入的部分是一条状态信息。在第5行我们创建了一个ActionMessages实例来保存状态信息,而在第12-14行和17-19行我们将ActionMessages保存到了request中,这样它们就可以在视图中被渲染。

下面给出Rails版本的update

code/people/app/controllers/people_controller.rb
def update
@person = Person.find(params[:id])
if @person.update_attributes(params[:person])
flash[:notice] = 'Person was successfully updated.'
redirect_to :action =>'show', :id => @person
else
render :action =>'edit'
end
end

实际的更新发生在第3行。ActiveRecord类的update_attributes方法可以一次同时修改多个属性。和createsave方法类似,update_attributes会自动进行验证。由于params[:person]这个哈希表包含了所有输入表单中的名/值对,update_attributes的一次调用完成了更新@person实例所需要的所有工作。

与Struts的更新类似,Rails版本的更新也设置了一条状态消息。在第4行,消息"Person was successfully updated."被加入到名为flash的特定对象中。通常,更新操作在结束时会重定向到其它action。那么如何在重定向过程中保证状态消息不会丢失呢?如果将状态消息保存到成员变量中,会导致这一消息在重定向后丢失。而使用session来作为保存机制虽然可行,但开发人员必须在随后执行清理session这一很容易被遗漏的操作。因此,Rails提供了flash作为解决方案。使用flash时,消息首先被保存到session中,以便本次重定向可以使用。而在下一次重定向后,Rails会自动在session中清理该消息。从而有效地解决了更新操作的状态信息在重定向时的保存问题。

flash的设计思想很巧妙。然而不幸的是,通常放入到flash中的数据则一点也不灵活。在Rails暂时还不支持国际化的情况下,状态消息却被直接以字符串(通常是英文)的形式存储。与之不同,在Structs应用中,状态消息被存储为一些诸如"person.added."这样的键值,其后视图可以用这些键值查找相应的本地化字符串。对国际化支持不足是Rails的一个重要缺陷。如果您的应用需要国际化支持,您必须自力更生或者使用第三方库。

在成功更新后,控制器应该重定向到执行“读操作”的URL。这降低了用户因为误收藏一个更新操作的URL而使更新操作重复执行的可能性。一些可选的“读操作”包括显示被更新过的对象信息,显示同类对象的列表,或显示顶层视图。在Structs版本中,重定向通过调用findForward实现:

return mapping.findForward("mainMenu");

为了确认这条跳转语句确实执行了重定向,您可以查看配置文件struts.xml。看起来一切如我们所愿:

<global-forwards>
<forward name="mainMenu" path="/mainMenu.html" redirect="true"/>
<!-- etc. -->
</global-forwards>

与Struts中findForward兼具渲染和重定向功能不同,Rails使用了两个单独的方法。在保存后,控制性显式的进行了重定向:

redirect_to :action => 'show', :id => @person

值得注意的是,此处重定向的定义包含了action和参数。Rails通过其后台的路由表,将action和参数转换为URL。当使用默认的路由规则时,所得到的URL为/people/show/(some_int)。

在展示了一个更新成功的场景后,我们来看一个更新失败的例子。Struts和Rails都提供了验证用户输入的机制。在Struts中,Validator对象基于XML配置文件中的声明自动验证form bean。验证规则是与表单相关联的。如果要指明某人的名字为必填项,您可以在XML中这样写:

code/appfuse_people/snippets/person_form.xml
<form name="personForm">
<field property="firstName" depends="required">
<arg0 key="personForm.firstName"/>
</field>
<!-- other fields -->
</form>

采用将验证规则分别写入到多个表单对应的配置文件中这种做法,其初衷在于分离关注点。不过有时将相关的关注点聚集在一起是一种更便捷的办法。在这里,我们使用在Person模型类中添加XDoclet annotations的方法来生成验证:

code/appfuse_people/src/dao/com/relevancellc/people/model/Person.java
/**
* @hibernate.property column="first_name" length="50"
* @struts.validator type="required"
*/
public String getFirstName() {
return firstName;
}

在Ant的构建过程中,这条struts.validator annotation将会在validation.xml文件中生成相应的代码。(在Java 5以及之后的版本中,annotation提供了更为简单和集成的注解机制)

Rails没有单独的form bean,而是直接在Person模型类中进行验证声明。您已经在第4.5节验证数据值中看到了这种做法。

code/people/app/models/person.rb
class Person < ActiveRecord::Base
validates_presence_of :first_name, :last_name
end

Struts和Rails采用了同样的方式来处理验证错误:重新渲染相应的页面,并将需要改正的表单输入域用错误信息标识出来。在Struts中,该页面转向由Validator负责。诸如PersonForm这样的Form bean继承了Struts类org.apache.struts.validator.ValidatorFormValidatorForm类提供了一个validate方法。Struct框架自动的调用validate,并在验证失败时重新渲染表单页面。

Rails所采用的方法则更为显式。当您调用ActiveRecord 模型的saveupdate_attributes方法时,如果验证失败将得到一个false的返回值。此时,您可以使用render来重新渲染用于编辑的表单页面:

code/people/snippets/update_fails.rb
if @person.update_attributes(params[:person])
# ...success case elided...
else
render :action => 'edit'
end

验证错误被存放在@person对象的属性errors中,所以您不需要作其他任何工作来将错误信息传递给表单视图。第6.5节创建HTML表单,介绍了如何在视图中渲染验证结果。

通过比较以上章节中Ruby代码和Java代码的相似部分,开发人员可以迅速通过示例掌握Ruby的用法。这种做法很有效,但同时也很危险,因为Ruby是一种与Java很不同的语言(如果您曾经读过用C语言风格编写的Java代码,您肯定能切实体会到这种危险!)有效使用Rails的重要条件之一是对Ruby语言拥有通彻的了解。因此,我们在本书中用两个章节单独介绍Ruby语言。书摘#2介绍Ruby语言最重要和强大的特性之一:核心类可修改。

书摘 2:扩展核心类

开发人员经常有向某一语言内建类中添加方法的需求。在这种情况下,继承这个类通常是行不通的,因为所添加的方法需要在基类的实例中可用。例如,Java和Ruby都没有判定一个String是否空白的方法,即为null、为空或只包含空格。由于许多应用都希望用同样的方式来处理各种空白字符串,因此这一判定方法是有用的。Java和Ruby的开源社区都提供了判定空白字符串的方法。下面给出Apache Commons Lang中isBlank()的Java实现:

code/Language/IsBlank.java
public class StringUtils { 
public static boolean isBlank(String str) {
int strLen;
if (str == null || (strLen = str.length()) == 0) {
return true;
}
for (int i = 0; i < strLen; i++) {
if ((Character.isWhitespace(str.charAt(i)) == false)) {
return false;
}
}
return true;
}
}

因为无法向Java核心类添加方法,Apache Commons Lang使用了标准的Java惯用法,即将扩展方法以静态方法的形式置入另一个类中。isBlank()的实现被置入StringUtils类中。

这样,isBlank()的使用者在每一次调用中都得加入辅助类StringUtils的类名作为前缀:

code/java_xt/src/TestStringUtils.java
import junit.framework.TestCase;
import org.apache.commons.lang.StringUtils;

public class TestStringUtils extends TestCase {
public void testIsBlank() {
assertTrue(StringUtils.isBlank(" "));
assertTrue(StringUtils.isBlank(""));
assertTrue(StringUtils.isBlank(null));
assertFalse(StringUtils.isBlank("x"));
}
}

Ruby类则是开放的——您可以在任何时候修改它们。所以,在Ruby中的相应做法是像Rails那样,将blank?加入到String中:

code/rails/activesupport/lib/active_support/core_ext/blank.rb
class String
def blank?
empty? || strip.empty?
end
end

下面是对于blank?的一些调用示例:

code/rails_xt/test/examples/blank_test.rb
require File.dirname(__FILE__) + '/../test_helper'

class BlankTest < Test::Unit::TestCase
def test_blank
assert "".blank?
assert " ".blank?
assert nil.blank?
assert !"x".blank?
end
end

null的处理

Java版本的isBlank()使用helper类StringUtils还有第二个原因。即使在Java中可以将isBlank()方法加入String类,我们仍然不应该这么做。因为当调用一个nullStringisBlank()方法时,期望得到返回值为false(译注:原文这里为false,但似乎应为true)。然而在Java中,企图调用一个null对象的方法将会引发一个NullPointerException异常。通过在StringUtils的静态方法中判断第一个参数,您可以避免在编写String方法的代码时必须加入诸如判断this是否为null这种无意义的代码。但问题是为什么Ruby的版本可以良好地进行工作呢?

Ruby中nil亦为对象

Rudy语言中的nil是Java中null的等价物。然而与之不同的是,nil是作为一个对象存在的。因此您可以像调用其它对象一样来调用nil对象的方法。对于编写isBlank()方法,更有意义的是,您可以像给其他对象添加方法一样,为nil添加方法:下面这段代码使nil.blank? 返回true

code/rails/activesupport/lib/active_support/core_ext/blank.rb
class NilClass #:nodoc:
def blank?
true
end
end

Rails同时为许多其他对象提供了合理的blank?定义:truefalse空数组及哈希表,数值类型,甚至是Object类本身。

目前存在众多语言和web框架。让Java脱颖而出的是其所在的整个“生态系统”。当我们启动一个新的Java项目时,我们知道任何我们遇到的问题都有很大的几率已经被Java世界中的开源项目解决了。对于任何像Rails这样的新兴框架,了解其周围的“生态系统”也是非常重要的。是否有优秀的IDE可用?是否可以方便的进行自动测试以及持续集成?对于常用的编程任务是否有标准库提供支持?

Java拥有无与伦比的强大“生态系统”,因此当您从Java迁移到其他语言时很可能遭遇一些挫折。但是我们发现Ruby的“生态系统”比我们想象的要丰富得多。在书摘#3中,我们将会介绍Ruby对于自动测试的支持,以及Rails如何扩展该支持来测试web控制器。

书摘 3:Rails中的Test::Unit扩展

学习Rails中的Test::Unit扩展时,最简单的入门方式是从Rails脚手架所生成的测试文件开始:

$ script/generate scaffold Person

我们已经在第1.2节Rails应用15分钟入门中了解过脚手架所生成的大部分内容。这里我们将着重介绍用于功能测试的生成文件:test/functional/people_controller_test.rb
下面我们逐行讲解PeopleControllerTest这个类。首先,该测试包含了一些夹具(fixture):

code/rails_xt/test/functional/people_controller_test.rb
fixtures :people, :users

脚手架在生成代码时会猜测PeopleController用于处理people模型类,因此它引入了相应的夹具。在很多应用中,一个控制器可能同时与多个模型类进行交互。在这种情况下,只需将其他模型加入到夹具声明中。例如:

fixtures :people, :widgets, :thingamabobs, :sheep

下面是setup方法:

def setup
@controller = PeopleController.new
@request = ActionController::TestRequest.new
@response = ActionController::TestResponse.new
end

几乎所有的功能测试都会模拟一次或多次的web请求/响应过程。因此,在每个测试中都需要@request@response的实例。举一个实际的例子。脚手架生成一个显示模型内容列表的index页面。 该页面所对应的测试如下所示:

举一个实际的例子。脚手架生成一个显示模型内容列表的index页面。 该页面所对应的测试如下所示:

def test_index
get :index
assert_response :success
assert_template 'list'
end

首先,get()方法模拟了一个发送给控制器的HTTP GET请求。这个方法有接收不同参数的多个版本,此处所使用的版本的参数为被请求Rails action的名称。其后的assert_response :success用于断言响应成功,即HTTP 状态码为200。而assert_template 'list'则用于断言返回页面由list模板所渲染。

作为Java程序员,我们很可能要问,“为什么没有用到那些在setup方法中生成的对象?”。或许test_index()更应该像下面的代码一样显式的使用这些对象:

# hypothetical, with explicit objects
@controller.get :index
assert_equal :success, @response.status
assert_equal 'list', @response.template

前面的这两个例子在功能上是能价的,只是风格有所不同。在Java中,我们趋向于显式的使用对象。而在Ruby尤其是Rails中,我们更喜欢尽可能的将那些显而易见的事情隐式化。请再比较一下上面两个版本的代码,我相信这次您可以更好的理解其中的差异。

接下来是用于测试list action的代码:

def test_list
get :list
assert_response :success
assert_template 'list'
assert_not_nil assigns(:people)
end

这段代码与test_index()大体相似,新加入的部分是下面这句:

assert_not_nil assigns(:people)

assigns是一个特殊的变量。如果您在控制器中创建了一个实例变量,那么这个变量就可以直接在视图模板中使用。这一机制背后的原理其实很简单:Rails首先通过反射将控制器中的变量拷贝到一个容器中,其后又将该容器中的变量拷贝回视图实例。而这个容器其实就是上面提到的assigns,所以前面的断言可以被理解为“控制器应该创建一个名为people的非空变量”。

下面到了show action的测试代码:

def test_show
get :show, :id => 1
assert_response :success
assert_template 'show'
assert_not_nil assigns(:person)
assert assigns(:person).valid?
end

这个测试看起来和前面几个有些不同,因为show方法需要指明所要显示的person实例。Rails的默认机制是通过在URL中加入id来标识特定的模型实例。因此,get()接收某一person实例的id作为第二个参数:

get :show, :id => 1

get()方法的一般形式可以用于处理各种场景下的请求:

get(action=nil, parameters=nil, session=nil, flash=nil)

但我们如何保证ID为1的person实例确实存在?让我们看一下相应的夹具文件

code/rails_xt/test/fixtures/people.yml
first:
id: 1
first_name: Stuart
last_name: Halloway

test_show()的另一个变化是valid?()

assert assigns(:person).valid?

这正是我们在第4.5节Validating Data Values所讨论过的,ActiveRecord对于验证的标准支持。随着您向Person类中加入验证方法,valid?()会自动的根据这些方法进行验证。

test_new()中没有什么新内容,所以我们这里就略过不讲,直接进入test_create()

code/rails_xt/test/functional/people_controller_test.rb
def test_create
num_people = Person.count
post :create, :person => {}
assert_response :redirect
assert_redirected_to :action => 'list'
assert_equal num_people + 1, Person.count
end

这里有几个值得注意的新内容。与前面讨论的shownew等众多方法不同,create方法会改变数据库内容。这一变化也对test_create的设计产生了一些影响。首先,由于create并不是一个幂等操作[1],测试中用post()代替了get()。其次,我们现在需要测试数据库的变化是否正确。下面的这行代码:

num_people = Person.count

获取了create()被调用前的person实例的数目。而这一句:

assert_equal num_people + 1, Person.count

则验证有且仅有一个person实例被创建。(如果您愿意的话,您还可以进行更严格的测试来保证新创建的person实例与创建时所传入的参数相匹配)

create()这样的非幂等操作所造成的第三个影响是我们不能再将:success作为预期响应。现在,创建成功后将重定向到show action。下面的代码:

assert_response :redirect
assert_redirected_to :action => 'list'

用于验证create()正确的进行了重定向。

剩下的脚手架测试方法(test_edit()test_update()test_destroy())也就没什么新内容了,不过阅读一下它们的代码还是有助于加强您对于Rails脚手架的理解。

为什么脚手架在一次POST请求后做重定向

在一次POST请求后做重定向可以防止用户在某些情况下做内容完全相同的重复更新。(您可能已经在一些写得不太好的web应用中见识过这一重复更新问题。一个标志性的表现是浏览器给出“您试图重复提交一个包含POST数据的URL。您确定吗?”)

Rails应用通常不会被重复更新问题所困扰,因为脚手架中内置了一个良好的解决方案(重定向)。

Rails构建于一门优秀语言(Ruby)和坚实的相关配套设施之上,是一个强大的web框架。当然许多其他的web框架也号称如此。那么为什么您应该将宝贵的时间用于学习和应用Rails呢?因为Rails程序员们正在又快又好的完成着各种任务。在开发人员生产率方面,Rails程序员们提出(并证实)了众多令人拍案叫绝的主张。而且,他们乐在其中。

Java程序员应该为Ruby on Rails的迅速蹿红而感动惊慌失措吗?当然不是。Java程序员在吸收利用Ruby on Rails方面具有天然而独特的优势。Rails for Java Developers会告诉您如何开始。


[1]一个幂等操作的特点是其任意多次执行所产生的影响均与一次执行的影响相同。幂等操作对于代理和缓存来说具有“友好性”,因为幂等操作的额外执行不会对二者产生危害性后果(除了带宽浪费)。幂等操作使用GET作为其HTTP动词

查看英文原文:InfoQ Book Excerpt: Rails for Java Developers

评价本文

专业度
风格

您好,朋友!

您需要 注册一个InfoQ账号 或者 才能进行评论。在您完成注册后还需要进行一些设置。

获得来自InfoQ的更多体验。

告诉我们您的想法

允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p

当有人回复此评论时请E-mail通知我

期待 by nangua xiao

感觉讲解很好,期待有下面有更多的新东西发布!

允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p

当有人回复此评论时请E-mail通知我

允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p

当有人回复此评论时请E-mail通知我

1 讨论

登陆InfoQ,与你最关心的话题互动。


找回密码....

Follow

关注你最喜爱的话题和作者

快速浏览网站内你所感兴趣话题的精选内容。

Like

内容自由定制

选择想要阅读的主题和喜爱的作者定制自己的新闻源。

Notifications

获取更新

设置通知机制以获取内容更新对您而言是否重要

BT