RoR 新人报到,Java 出身,正开始学 Ruby 和 Rails,特此感慨,欢迎前辈们批评指正。
Markdown 格式可能有点乱,见谅!
原文:http://dylanninin.com/blog/2013/11/11/for_rails.html
我是一枚 Java 攻城狮,工作两年多,主要做 Java Web 开发,期间转去做了一年多的Oracle DBA,维护Oracle EBS,也当过Linux Administrator。
Sun 早就被 Oracle 收购了,Java 和 MySQL 成了 Oracle 的小儿子;Oracle Database 和 EBS 自不必说,Oracle 的亲亲亲生孩子,庞然大物,组件众多;Oracle 推出了基于 Redhat 的 Oracle Linux, 号称 Unbreakable,所以 Linux Administrator 就成了 Oracle Linux Administrator。看这趋势,一路朝 Oracle 走来一路坑,深有被 Oracle 绑架的感觉,所以希望能够从 Oracle 中解脱出来,呼吸下 新鲜空气。
最近王垠新写了一篇博文再见 Voxer,你好 Sourcegraph,讲述他最近的一段经历和想法,顺便小议了下 Oracle:“想当年 Oracle 的那些烂东西也是用同样的方式发家的吧”。 这是不是在黑 Oracle,我想不是的。在公司刚结束的一个由华南地区 Oracle ACS 的 team 主导的 Oracle EBS 升级项目中,有一位技术深厚、经验丰富的顾问(专攻 Oracle Database,经历过 8i, 9i, 10g, 11g,现在 12c 开始着火了)半开玩笑的说道:Oracle Database 就是一堆 Patch 堆积起 来的。这是在食堂排队前的一点闲谈,但不无道理,以不才一年之误尚不足以体会这么深,可顾问该是有些说服力的。
甲骨文叫 Oracle,而不取类似“Apache”的名字,是不是很神秘?即使这样,公司每年还是要签 Oracle 的 License,为 Oracle 测试、提 Bug。
在做 Oracle DBA 时看了阮一峰的Java 语言学校的危险性 (译文),似乎识迷途其未远;偶尔碰见Lisp 的永恒之道,编程语言真的会影响人的思维方式;昨天还看了下 Hacker News 上的提问Ask HN: Is Java still worth learning?又验证了 某些想法。现在开始觉得是时候换种语言了,在同学、朋友的影响与帮助下,开始了RoR之旅。
##Java
先简单总结下个人 Java Web 开发的经历。基本有三个阶段:
工作中接触的 Java Web 项目,都是采用 SSH 的基础框架。偶尔还有点变化,前端从 Adobe Flex 到 Ext JS3,Java FX 也曾一度惊现,但缺少纯 HTML 和 JavaScript;开始集成 Lucene 做简单的站内搜索,或者干脆使用 Solr;最近又要集成 IM,做基于 Openfire、Spark、FastPath 的开发;用够了 Hibernate,终于开始调研 MyBatis;听说 PHP 和 Java 可以强强联合,所以又开始尝试 Quercus 及 php-java 的桥接... ...
以个人有限的知识和经历,我认为对中小型企业来说 SSH 框架已经基本够用。作为补充,上个月整理了一份Java Resource可供参考。
##Rails
以上是从大学到工作至今接触过的一些 Java Web 开发的技术或框架,它们堆积在一起很吓人,说不上完全掌握,但理解还是有一点的;要说是啃老族,那我啃的还是大学的底,只是不再认为“Java Web == SSH”。
在回头审视这些项目或阅读源代码时,我有经历过一些想法的摩擦和碰撞,而这些想法又体现在 Rails 中,但 Rails 做得很好,也很优雅,当之无愧的"魔幻"。
看 Rails 的第一本书是Agile Web Development with Rails 4,它除了教你如何认识和使用 Rails,也讲了敏捷开发的流程,不得不说让人感受和启发良多。如果要说一句话,我想那会是相见恨晚。
##Java vs Rails
下文将说说这几天学 Rails 时脑海中时而涌现的念头,作为对 Java Web 开发的一次小结,也算是对 Rails 的一种拥抱。
这些想法很基础,可以说任何一个项目都会遇到;但也很重要,我认为只有理解和解决了它们,一个项目的基石才是可靠的。
主要包括:
特别说明:
##项目结构
###目录结构
####Java
Java Web 应用并没有明确规定该采用怎样的目录结构,唯一可寻的规范似乎是WAR file format中提及的web.xml,其中 web.xml 的具体内容又由web-app_2_5进行定义。
一个打包好的 Java Web 应用应有如下的目录结构,以便部署到 Tomcat、Weblogic 等容器中。
WebRoot/
css/
images/
js/
logs/
META-INF/
WEB-INF/
classes/
lib/
web.xml
index.jsp
404.jsp
500.jsp
项目开发的目录结构就真没有规范可寻了,最简单的只需要一个 src 目录和一个 WebRoot 目录即可搞定;但随着时间推移,或许是半年,一年,也或许是两年,这样的目录结构并不能满足需求。于是经过调整,可能会发展成这样:
my_project/
src/
java 源码包,一般按域名继续划分,防止包冲突,如 com.egolife.ssh;
test/
java 测试源码包;
resource/
各类资源或配置文件,如 struts, spring, system properties, freemarker templates, jbpm processes;
doc/
项目文档目录;
lib/
测试时需要的额外的库,如 jsp,servlet,junit,spring 等;运行时需要的 lib 在 WebRoot 下;
单独列出此 lib,方便正式部署时排出这些包;
reports/
报告目录,如单元测试报告;
WebRoot/
Web 根目录,包含运行所需的 web.xml,lib,classes 等;
build.xml - Ant build 文件;
但也可能会是这样:
my_project/
src/
java 源码包,一般按域名继续划分,防止包冲突,如 com.egolife.ssh;
test/
java 测试源码包;
config/
各类资源或配置文件,如 struts, spring, system properties, freemarker templates, jbpm processes;
lib/
测试时需要的额外的库,如 jsp,servlet,junit,spring 等;运行时需要的 lib 在 WebRoot 下;
单独列出此 lib,方便正式部署时排出这些包;
sql/
数据库 schema,或者实用的 script;
WebRoot/
Web 根目录,包含运行所需的 web.xml,lib,classes 等;
####Rails
Rails 框架已经约定好了项目的目录结构,你只需要输入 rails new my_app
,它就会自动帮你生成,每个文件/目录的定义清晰可见。
my_app/ app/ Model, view, and controller files go here. bin/ Wrapper scripts config/ Configuration and database connection par config.ru - Rack server configuration. db/ Schema and migration information. Gemfile - Gem Dependencies. lib/ Shared code. log/ Log files produced by your application. public/ Web-accessible directory. Rakefile - Build script. README.rdoc - Installation and usage information. test/ Unit, functional, and integration tests, fixtures tmp/ Runtime temporary files. vendor/ Imported code.
###包或模块结构
除了项目的顶级目录结构,包或模块结构的划分往往是一件令人头疼的事儿。
最早开始注意到这个问题,还是在 11 年工作后开发第一个 Java Web 应用时。那是四个人的小团队针对老系统进行再次开发,抱着动大刀的心态,总觉得原有项目的包结构十分别扭,修改一个模块时经常需要跨越多个包,而且前端还使用了 Adode Flex 3 的框架,IDE 提供的 Refactor 功能顿 时失灵了,在修改 service 的一个方法名、model 的一个属性时别提会有多么恶心(深受其害之后动手写了一个小工具,可以批量将 Java 的 Model 映射成 Flex 的 valueObject,copy->paste->fix 这种方式实在太 ugly)。当然,很有可能是再次开发增强了这种恶心感,手里拿着锤 子什么的,那自然一切都成了钉子。不过,开发第二个 Java Web 项目时,还是沿用了原有 的包结构,SSH 框架,前端由 Adobe Flex 3 换成了 Ext JS3,但明显没有以前那种恶心感。如果这个系统再由一批新人去维护,很有可能他们也会受罪,正如我们一样,只不过这次是我们制造的。
言归正传,还是谈谈 Web 项目的包或模块结构,先引一篇老文,JavaEye新闻月刊 2008 年 07 月总第 05 期四个有害的 Java 习惯之包的命名和划分。
####包的命名和划分
包 (package) 的命名和划分按照行为和层次划分 (package-by-layer),而不是根据特征和功能划分 (package-by-feature)
这个问题在我刚学 java 的时候就遇到了,在看了众多的网上开源程序后,我也慢慢习惯了按层次命名包。
作者举了个例子:
com.blah.action com.blah.dao com.blah.model com.blah.util
我们已经习惯了按照层次分类或者叫按照行为分类,model 一个包,dao 一个包,service 一个包,action 一个 包。这样就把具有同样特征或者功能的类划分到了不同的包里。这样的习惯,把 java 的包内私有 (package- private) 这个作用域给完全扔掉了,而包内私有是 java 的默认作用域(ps:我学 java 来好像很少用过 java 的包内 私有这个作用域,汗一个)。
这种包的划分习惯也违反了面向对象编程的核心原则之--尽量保持私有以减少影响,因为这种习惯强迫你必须扩大类的作用域。
下面的包命名方式是按照特征划分命名:
com.blah.painting com.blah.buyer com.blah.seller com.blah.auction com.blah.webmaster com.blah.useraccess com.blah.util
举个例子,在一个 web 应用中,com.blah.painting
包可能包含下面的成员:
Painting.java: model 对象 PaintingDAO.java: dao 对对象 PaintingAction.java: controller or action 对象 statements.sql: SQL 文件 view.jsp: JSP 文件
值得注意的是这种情况下,包里包含的不仅仅是 java 源码文件,同时也包含其他与该特征相关的文件。这点上好像违反大多数 java 程序员的习惯,并且如果要打包为 jar 好像也不方便,真实环境中如何应用,有没有别的麻烦,还要待实践一下。
作者列举了这种包划分方式的优点:
作者引用了一句 Effective Java 中的名言:
"The single most important factor that distinguishes a well-designed module from a poorly designed one is the degree to which the module hides its internal data and other implementation details from other modules."
-- Joshua Bloch, Effective Java
####Java, Rails and Django
在按照行为和层次划分还是根据特征和功能划分的争论上,我们的 Java 项目优先选择了前者,在 MVC 层级的结构之下再按特征和功能划分模块;Rails 与之类似。那么,有没有反过来的呢,即正如上文作者提到的建议,先按特征和功能划分,再按行为和层次划分。答案是 Django,在 Django 1.5.3 Documentation 中提到的项目结构:
tutorial/ mysite/ init.py settings.py urls.py wsgi.py polls/ init.py urls.py views.py models.py admin.py tests.py static/ polls/ style.css templates/ 404.html 500.html polls/ detail.html index.html result.html hn/ init.py urls.py views.py models.py admin.py tests.py static/ hn/ style.css templates/ 404.html 500.html hn/ detail.html index.html result.html templates/ admin/ base_site.html manage.py
目录结构,包结构十分重要,它们从文件、模块的层次体现着项目的设计水平和权衡,在这方面感觉 Ruby 和 Django 要略胜一筹,因为设计或不设计,它就在那里。
##命名规范
对程序猿来说,Coding 并非是最难的事情;那是什么?命名!!!对,是命名偷走程序猿的大把时间和精力。
记得在一个 OA 项目开始初期,整理过一分 Java 和 Ext JS 的编码规范,其中命名规范是重点,实践起来实际上是困难重重。想一想,单是找出一个合适的英文单词 似乎就要绞尽脑汁,随意和不杀脑细胞的后果是瞅瞅国内一些开放平台的 API 就知道他们的英语水平和设计素养;当然,如果你直接使用中文编程就另当别论了。
###Java
Java 命名规范举例(参考了 AWDR4)
Model Naming
Table image_sprite
Package com.egolife.ssh.po.media
Class ImageSprite
File my_project/src/com/egolife/ssh/po/media/ImageSprite.java
ORM my_project/src/com/egolife/ssh/hbm/media/ImageSprite.hbm.xml
Service Naming
Package com.egolife.ssh.service.media Interface ImageSpriteService Method ImageSprite findById(String id); File my_project/src/com/egolife/ssh/service/media/ImageSpriteService.java
Class ImageSpriteServiceImpl Method public ImageSprite findById(Srting id){ ... ... } File my_project/src/com/egolife/ssh/service/media/impl/ImageSpriteServiceImpl.java
Spring
File my_project/resource/spring/applicationContext-media.xml
Action Naming
URL http://../media/findImageSpriteById.action?id=DEADBEEF Package com.egolife.ssh.action.media Class ImageSpriteAction Property private String id; Method public String findImageSpriteById(){ ... ...} File my_project/src/com/egolife/ssh/action/media/ImageSpriteAction.java
Struts File my_project/resource/struts/struts-media.xml
除了 Java 文件,还涉及到 Struts, Hibernate, Spring 一堆 XML 的配置文件,当然,你也可以使用 Annotation 实现“零配置”。
###Rails
Rails 命名规范举例(直接引用 AWDR4):All these conventions are shown in the following tables.
Model Naming
Table line_items File app/models/line_item.rb Class LintItem
Controller Naming
URL http://../store/list File app/controllers/store_controller.rb Class StoreController Method list Layout apps/views/layouts/store.html.erb
View Naming
URL http://../store/list File app/views/store/list.html.erb (or .builder) Helper module StoreHelper File app/helpers/store_helper.rb
###ORM 命名策略
由于关系数据库与面向对象的不匹配,ORM 应运而生。在 Java 中比较典型的 ORM 框架是 Hibernate,采用 XML 文件描述 Java POJO 和数据库表之间的对应关系,可以让开发人员以面向对象的方式来完成 Java 与数据库的交互,而不再纠缠于 JDBC,写大量的原生 SQL 和参数的 setter/getter;是的,这时最满意的要数程序员的 手指头。在 Rails 中,ActiveRecord 担任了此角色,但省去了 XML 文件,这时你会更觉满意。
随着 ORM 的出现,Model 和 Table 的命名问题也逐渐浮出水面:
在以 SSH 框架为基础的 Java Web 项目开发中,一般有两种选择:
在接触过的项目中,以 ddl2java 的选择居多,逆向工程可以帮忙生成很多代码,这是自底向上的设计思路。
无论是 ddl2java,还是 java2ddl,都有一个叫NamingStrategy的工具 在帮你进行一些默认的转换,毕竟 Java 和数据库各有各的命名规范,迁就哪一个都觉得有所不妥。
在 Rails 中同样有 NamingStragety,只不过这是一个很隐匿的家伙,约定俗成以至于不需要它了。
关于 Model 和 Table 的命名问题,最大的要数单数还是复数了。Java Web 开发中暂未确定,随开发人员高兴,但一般还是挺一致的,要么都采用单数,要么都采用复数,但这样又似乎确凿有些别扭:
List<Users>
,Users 不是已经是复数了吗,这时你不会有一种使用 foreach 嵌套循环的冲动?这样似乎也有些不妥?终极方案似乎是这样的:
这问题在 AWDR4 中解释得很好,摘抄如下:
David says: Why Plurals for Tables?
Because it sounds good in conversation. Really. “Select a Product from products.” And “Order has_many :line_items.”
The intent is to bridge programming and conversation by creating a domain language that can be shared by both. Having such a language means cutting down on the mental translation that otherwise confuses the discussion of a product description with the client when it’s really implemented as merchandise body. These communica- tions gaps are bound to lead to errors.
Rails sweetens the deal by giving you most of the configuration for free if you follow the standard conventions. Developers are thus rewarded for doing the right thing, so it’s less about giving up “your ways” and more about getting productivity for free.
##日志处理
先讲一个笑话,第一次见到Log4j,你会怎么读?"Log|Four|J",还是"Log|四|J"?饱受中文和阿拉伯汉子熏陶的 Java 程序猿,若是第一次碰到,不出意外,大都 会读错,即读成 "Log|四|J",在我不长的 Java 开发生涯里,已经碰到好几例。
即使联想到"J"是"Java"的缩写,也难以想到"4"代表什么;但若知道这世上还有 Log4cxx,Log4php,那大概可以猜到"4"其实代表"For",只是"LogForJava"写起来太长了,"Java"缩写成"J","For"干脆就用"4"代替了;更进一步,可以说 Log4j 所代表的是一种思想、一种设计,只不过刚好是 Java 语言的实现。这和"B2B"、"B2C"是一个道理,但君不见也有某主持人把"B2B"读成"B 二 B"的。
调试 Java 程序,难免会有打印输出,使用System.out.println(message)
和System.err.println(error)
就可以满足需求;但你一定干过这事儿:1)第一版的代码里为了调试写满 println,调试好后觉得输出的无关信息太多,看着怪怪的,手痒了就非常勤快地把它们删掉;但使用时还是莫名地出错,于是又手贱地再写了一遍println
。2)正式上线前,经理突然说把项目里的调试代码注释掉,以免影响运行的性能;好家伙,这一天没写一行代码,反倒删除了几千行。
这时,Log4j 可以帮你免除手痒手贱之苦。使用 Log4j 可以自定义输出级别,方便控制项目在测试阶段、上线运行时的日志输出;你也可以在日志输出到标准输出的同时,保存一份到文件或数据库中,以便定时查看和统计分析;碰到 Error/Fatal 类的错误时,发送一封日志邮件给开发维护人员也很不错,何乐而不为呢。
Log4j 是很多 Java 程序里默认的日志组件,在 Hibernate 逆向工程生成的 DAO 里即可找到它的身影。看起来很不错,但要在自己的 Java 代码用到,还是得稍微花点功夫。这时你需要引入 Log4j 的配置,熟悉 rootLogger、LogLevel、Appender、Layout 等概念,若有兴趣,似乎也想看下 Log4j 的设计原理和具体实现,趁机“Pick Up”一些设计模式,然后为己所用。不仔细看文档,直接上配置和代码的人可能要折腾一小会儿了,好在打造一份 Log4j.properties 之后,就可以随你“Copy and Paste”。
Rails 中有类似的日志组件,叫 Logger -- supporting rollover,零配置即可用;当然了,遇到项目上线运行时还是需要定制下的。只是在此不得不感叹下,在 Rails 中使用 Logger 似乎出乎天性、理所当然,但 Log4j 却要一些折腾,一些配置。
Gists:
##单元测试
在最初的 Java Web 开发里,是没有单元测试的概念的,即使到现在开发人员也依然习惯说:功能我写好了,你看!
jUnit 是什么?为什么要单元测试?即使我看了 jUnit in Action,理解测试的重要性,也不见得会按部就班的进行严格的单元测试。
在 Java Web 项目中,可以借助 IDE 自动生成单元测试用例,虽然功能有限,但也省了不少时间(如果连这个功能都不知道,那可以说你还生活在原始时代)。出于对 IDE 自动生成的单元测试用例的不满,自己写过两个版本的小工具。一个是 Java 版的,粗野的将文本扫描和正则表达式相结合,渲染 freeMarker 模板,批量生成 TestCase 和 Tests,并自动填充 Parameters 和 Asserts,接下来所要做的工作就是更改 Parameters 和 Asserts,并运行测试即可。第二个是 Python 版的,最初是想用 Java 重写,但还是 脱离不了扫描源文件(想过使用反射来实现,但看 Java 提供的反射 API,运行时源代码中定义的参数名丢失了,实在不忍用 arg1, arg2 来代替 start, pageSize),当时正好在学 Python,所以改用 Python 重新实现。代码只能捂脸见人,所以就不贴出来了,思路在模板,见jUnit TestCase Gist。
对涉及到数据交互的单元测试,jUnit 已经不够用了。当时找到一个叫 DbUnit 的小框架,是 jUnit 的扩展,可以用 XML 事先构建一些测试数据集,在单元测试的时候载入数据库,测试完成后从数据库中清除,有效的控制了测试数据库的状态。使用 DbUnit 大概有两个好处:1)构建测试数据集,可重复测试,再也不用担心测试数据被修改或丢失;2)测试完成后清除测试数据,保证了测试前后数据库的数据是一致的。
当然,使用 jUnit 已经很折腾,加上 DbUnit,好像又得花几天的时间了。
那么,Rails 中是怎么解决的呢?
Rails 提供了 scaffold 和 generator,可以自动生成测试用例,而且把我想通过编写小工具去实现的功能完成得更加优雅;在构建测试数据时,Rails 一致地选择了 YAML,对了,这应该称作 Fixtures,你可以在 test/fixtures 目录下找到它们。
YAML 是什么呢,YAML is a recursive acronym for "YAML Ain't Markup Languade". Early in its development, YAML was said to mean "Yet Another Markup Language", but it was then reinterpreted(backronyming the original acronym) to distinguish its purpose as data-oriented, rather than document markup. 详情请见Wikipedia: YAML。
为什么不使用 XML,因为 YAML 面向数据、自解释,你再也不用写一堆标签和属性。
这还不是最重要的,最重要的是,在这种内在的测试支持的帮助下,你会很乐于做这件事而无需额外折腾,正文 AWDR4 文中所言“One of the real joys of the Rails framework is that it has support for testing baked right in from the start of every project.”
##构建工具
Java 中用于自动化构建的工具有 Ant 和 Maven。Ant 起步比较麻烦,需要手写一大堆东西;Maven 无需配置,可直接上手使用。但 Maven 的网络特性更强一些,在隔离互联网的内网开发环境下是难以生存的,所以要用也只用到 Ant。
Ant 的介绍:Apache Ant is a Java library and command-line tool whose mission is to drive processes described in build files as targets and extension points dependent upon each other. The main known usage of Ant is the build of Java applications. Ant supplies a number of built-in tasks allowing to compile, assemble, test and run Java applications. Ant can also be used effectively to build non Java applications, for instance C or C++ applications. More generally, Ant can be used to pilot any type of process which can be described in terms of targets and tasks.
Ant 可以编译、打包、测试和运行 Java 程序,但不限于此,你还可以编写自己的扩展,加入到 Ant Lib 中,实现更多更丰富的任务,比如创建 Java 项目的目录和包结构;一些开源的项目也提供了 Ant Lib,比如 Hibernate,jUnit。
从头开始写一个 Ant 配置有点麻烦,但写好之后基本可以复用,有不一样的地方只需根据项目需求进行调整即可,这样似乎有点一劳永逸了,也似乎有点不够优雅,更别提创建一个新项目的时候你还记得有这事儿吗?踩进与 Ant 有点关系的深坑,要数实习期间的一个 Web 项目,前端使用 Adobe Flex 3,在给 MyEclipse 装上一大堆插件之后,编译 Flex 代码变得奇慢无比;若是你不小心勾选了自动编译的选项,Flex 代码的任何一点改动都可以让你等上一二十分钟。这时想到了 Ant,查到 Ant 和 Adobe Flex 的官网,确实提供有编译 Flex 的 Lib,也有一些教程,接下来就是测试和移植工作了。那会儿顿时觉得很兴奋,希望一天内就能完成,实际上零零散散花了一周多的时间还没搞定,记得好像是某些 style 资源始终无法编译到 SWF,最后就不了了之。
Gists:
那么 Rails 表现如何呢?Rails 提供了 Rake,比起 Ant 需从头编写 build.xml 以及使用 Flex Task 时遇到的困难,Rake 更像 Maven,内置了很多任务,无需配置;到目前为止,我还没踩进 Rake 的坑。
Rake 的介绍:Rake is like having a reliable assistant on hand all the time: you tell it to do some task, and that task gets done.
Rake 在你键入"rails new my_apps"时,已经陪伴你左右,随时待命。见Rake Gist。
是的,Rake 是一个值得信赖的好助手,随时随地就在身边供你差遣;而 Ant 需要你手工打造,好与坏完全取决于自身的水平。
##参考