2005年12月11日
Currently the concept of Progressive Enhancement is getting hotter and hotter. It emphasizes accessibility, semantic markup, and the importance of separating the complex rich interaction logic into well modularized javascript files. It's really a usefully way of thinking about how to modularize and manage web presentation components. But the Rails framework doesn't have good support for PE, so we have to define our own convention and helpers to make our life easier.
Usually, I'd like to organize js files in the Rails convention, which means we'll have something like this:
app
|
- views
|
- admin
|
_ new.html.erb
- index.html.erb
public
|
- javascripts
|
- admin
|
- new.js
- index.js
And new.js looks similar to:
$(document).ready(function() {
enhanceInteractionOnElements();
![](http://www.blogjava.net/Images/dot.gif)
});
function helper_methods() {
![](http://www.blogjava.net/Images/dot.gif)
}
Then, add the follow method to ApplicationHelper module:
def page_javascript_include_tag
file = "#{params[:controller]}/#{params[:action]}.js"
File.exist?("#{RAILS_ROOT}/public/javascripts/#{file}") ? javascript_include_tag(file) : ""
end
this method will look for js file for a particular page. And in you layout file, add one line in the head sectin:
<%= page_javascript_include_tag %>
That's it. Whenever you request an action of a particular controller, it will find and include the PE js files automatically. Now we've very very primitive support of PE in Rails framework now.
前几天在JavaEye海阔被标题党阴了一把,看了一篇转的文章叫《 被中国人误传了数千年的七句话》,颇有些哭笑不得的感慨:
1. 这些话的确是被误传了不假,但是最多也就一百年吧。中国知识分子不读四书五经史子集的坏风气大抵是开始于所谓的新文化运动吧。再往前的人,对于这些典籍字字爬梳,提了上句马上背下句,就算是以章句式解读为主的宋元,也不应该随随便便就被忽悠了,更不用说反对宋儒理学讲究正本清源的明清了。
2. 古人断章取义是一种风雅的言谈习惯,所谓“雅言”是要字字出典的,有点像对暗号。比如我们家猫跑了,搁古代我肯定问“谁之过欤?”,十有八九会回答说,“言是典守者之过也”,这句射的是“虎兕出于柙”,正好应景。甚至为了诙谐应景,故意曲解文义的情况也是很常见的。如果以此为证说误传的话,恐怕只能算是牛嚼牡丹了。顺便多说一句,其实这个毛病现代人也有,不过不再是古文了,大多数是电影电视台词:“空气在颤抖仿佛天空在燃烧。是啊,暴风雨就要来了”,“道哥,牌子啊”,“你看我的英语,有没有长进”之类的,虽不复古韵,但也还算有趣。
P.S. : 今天team里有人把David Wheeler的名言,贴在了Quote Wall上:“Any problem in computer science can be solved with another layer of indirection.”
这到的确算是一句被误传的名言吧,原文是“Any problem in computer science can be solved with another layer of indirection. But that usually will create another problem.”
Aurum is a Ruby-based LALR(n) parser generator that you can use to develop your own domain specified languages, scripting languages and programming languages.Although it's just yet another parser generator, Aurum is slightly different from other widely used parser generators:
- One of major targets of Aurum is to simplify external DSL development, espectually Ruby external DSL.
- Aurum uses incremental LALR(n) algorithm instead of the common used LALR(1)/Full LALR(n) algorithm. That means:
- Allowing the user to express grammars in a more intuitive mannar.
- Making it easier to handle complicated grammars. For exmaple,
COBOL(LALR(2 or 3)), simplified nature language(LALR(3+)) and etc.
- Closer to Generalized LR in language recognizing but much more faster.
- Smaller parsing table comparing to Full LALR/LR(n) algorithm.
- Aurum supports grammar reuse, and itslef'll be shipped with some pre-defined common structures. One of the pain points of external DSL is that you have to re-define lots of common structures, such as if statements, block structure and etc. With Aurum, you could simply reuse them.
- Aurum uses a Ruby interal DSL as meta-language, and provides a generic lexer/parser as well. You could test your grammar by the comprehensive testing libraries Ruby has(you could even develop your lexer/parser in the TDD fashion).
- As the name suggested, Aurum, the Latin word for Gold, is partially inspired by the GOLD Parsing System. The grammar you created with Aurum could be completely independent of any implementation language,even Ruby.(not implemented yet :) )
Ok, let's start from the 'Hello World in Compiler Construction' —— Expression Evaluation
1 require 'aurum'
2
3 class ExpressionGrammar < Aurum::Grammar
4 tokens do
5 ignore string(' ').one_or_more # <= a
6 _number range(?0, ?9).one_or_more # <= b
7 end
8
9 precedences do # <= c
10 left '*', '/'
11 left '+', '-'
12 end
13
14 productions do # <= d
15 expression expression, '+', expression {expression.value = expression1.value + expression2.value} # <= e
16 expression expression, '-', expression {expression.value = expression1.value - expression2.value}
17 expression expression, '*', expression {expression.value = expression1.value * expression2.value}
18 expression expression, '/', expression {expression.value = expression1.value / expression2.value}
19 expression '(', expression, ')' do expression.value = expression1.value end # <= f
20 expression _number {expression.value = _number.value.to_i}
21 expression '+', _number {expression.value = _number.value.to_i}
22 expression '-', _number {expression.value = -_number.value.to_i}
23 end
24 end
If you has any experience with other compiler compiler/parser generator, you probably could understand what happens above quite easily. Instead of explaining things like token, character class, and production, I'd like to emphasise some Aurum conventions:
- At point a, we use 'ignore' directive to declare the ignored pattern, such as whitespaces etc.'string' is one of the helper methods(others are enum, range and concat), which is used to define lexical patterns. It will create a pattern matching the given string exactly.
- At point b, we declare a lexical token named '_number'. In Aurum, lexical tokens, or terminals from syntax perspective, always start with '_'. The expression '_token_name pattern' is equivalent to 'match pattern, :recognized => :_toke_name'. The 'match' directive is a common way to associate lexical action with leixcal pattern.
- At point c, we declare operator precedences of the Expression grammar.The eariler the operators definied, the higher precedence they will have.
- At point d, we declare syntax rules of Expression grammar. According to Aurum naming convention, all terminals should start with '_' while all nontermainls start with lower case alphabet character. String literals will be interpreted as reserve words, and added to lexer automatically.
- At point e, we define a semantic action to the Addition rule. In semantic action, you could access to the objects in value stack via the name of corresponding symbols.If there are more than one symbol with the same name, you could differentiate them by the order they appered in the production.
- At point f, we use do..end instead of {..}. Using Ruby internal DSL as meta-langauge is a double-side sword, you have to bear its flaws while enjoying the remaining parts. There is no perfect world, isn't it?
Now, let's find out how we could use this expression grammar. You could use the helper method as below(it will recalcuate lexical table and parsing table for every call, could be quite slow):
1 puts ExpressionGrammar.parse_expression('1+1').value
or use the lexical table and parsing table to create your own lexer & parser:
1 lexer = Aurum::Engine::Lexer.new(ExpressionGrammar.lexical_table, '1+1')
2 parser = Aurum::Engine::Parser.new(ExpressionGrammar.parsing_table(:expression))
3 puts parser.parse(lexer).value
At the end of this post, I'd like to give another grammar example coming from Martin Fowler's HelloParserGenerator series:
1 require 'aurum'
2
3 Item = Struct.new(:name)
4
5 class Catalog < Aurum::Grammar
6 tokens do
7 ignore enum(" \r\n").one_or_more
8 _item range(?a,?z).one_or_more
9 end
10
11 productions do
12 configuration configuration, item {configuration.value = configuration1.value.merge({item.value.name => item.value})}
13 configuration _ {configuration.value = {}}
14 item 'item', _item {item.value = Item.new(_item.value)}
15 end
16 end
17
18 config = Catalog.parse_configuration(<<EndOfDSL).value
19 item camera
20 item laser
21 EndOfDSL
22
23 puts config['camera'].name
P.S.:The post is based on the developing version of Aurum(0.2.0). You could get it from the svn repository.
P.S.P.S.: There is a more complicated example in the examples directory, a simple Smalltalk interpreter. Have fun:)
A very brief introduction to Aurum
Aurum是一个用Ruby实现的LALR(n) parser generator(是的,又是一个parser generator),不过它和其他一些广泛应用的parser generator相比略有不同的:
1.Aurum的主要目标之一,是简化external DSL的开发(尤其是ruby external DSL)。
2.Aurum采用增量LALR(n)算法,而不是通常的LALR(1)。这意味着:
a.不必由于LALR(1)能力的限制,而改写语法,很多在LALR(1)中冲突的语法在LALR(n)中可以比较自然地表达。
b.由于识别能力的增强,可以处理一些比较复杂的语法,比如COBOL(LALR(2)或LALR(3)),比如一些简化的自然语言(LALR(3+))。
c.处理能力接近Generalized LR,却快很多
d.比起Full LALR/LR(n),增量算法生成的语法表更小。
3.出于简化external DSL实现的考虑,Aurum支持语法重用。
4.Aurum采用Ruby internal DSL作为语法声明的元语言,可以利用Ruby丰富的测试框架,有效地对编译/解释/分析器进行测试。
5.正如名字所暗示的,Aurum(Gold的化学名称)的一部分灵感来自 GOLD parsing system,它将支持独立于平台和语言的编译器开发。
好,闲话少说,看一个例子,编译原理中的Hello World —— 表达式求值:
1 require 'aurum'
2
3 class ExpressionGrammar < Aurum::Grammar
4 tokens do
5 ignore string(' ').one_or_more # <= a
6 _number range(?0, ?9).one_or_more # <= b
7 end
8
9 precedences do # <= c
10 left '*', '/'
11 left '+', '-'
12 end
13
14 productions do # <= d
15 expression expression, '+', expression {expression.value = expression1.value + expression2.value} # <= e
16 expression expression, '-', expression {expression.value = expression1.value - expression2.value}
17 expression expression, '*', expression {expression.value = expression1.value * expression2.value}
18 expression expression, '/', expression {expression.value = expression1.value / expression2.value}
19 expression '(', expression, ')' do expression.value = expression1.value end # <= f
20 expression _number {expression.value = _number.value.to_i}
21 expression '+', _number {expression.value = _number.value.to_i}
22 expression '-', _number {expression.value = -_number.value.to_i}
23 end
24 end
如果诸位对之前有用过compiler compiler或者parser generator的话,应该能看个七七八八吧。我大概解释一下:
a.这里定义了文法空白,也就是被lexer忽略的部分,在通常的语言中,是空格回车换行之类的字符;string是用于定义lexical pattern的helper方法(出了string之外,还有range, enum和concat);ignore是一个预定义的说明指令,表示若文本匹配给定模式则该文本会被lexer自动忽略,其格式为:
ignore pattern {//lexical action}
b.此处为lexical token声明,所有lexical token必须以_开头,其格式为:
_token_name pattern {//lexical action}
这里其实是一个简略写法,等价于
match pattern, :recognize => :_token_name
c.此处为运算符优先级声明,支持左/右结合运算符(无结合属性运算符开发中);每一行中所有运算符具有相同优先级;比它下一行的运算符高一个优先级。比如在这个例子中,'*'和'/'具有相同优先级,但是比'+'和'-'的优先级别高。
d.此处为语法规则声明,所使用的symbol主要有三种,nonterminal(小写字母开头),terminal(其实就是lexical token,以_开头)和literal(字符串常量),其中所有literal都会被自动声明为保留字。
e.此处定义了一条文法规则(加法),以及对应的semantic action。在semantic action中可以直接通过symbol的名字来获取值栈中的对象。如遇到同名symbol,则按照出现顺序进行编号即可。
f.其实这个没啥,只不过由于我们使用的是Ruby DSL,所以有时候不能都用{},需要do end,这就是一个例子。
最后测试一下实际中如何使用定义好的语法(使用helper method,注意由于分析表没有缓存,每次都会重算语法表,仅仅适用于debug mode。)
puts ExpressionGrammar.parse_expression('1+1').value
或者通过分析表自己构造lexer和parser
lexer = Aurum::Engine::Lexer.new(ExpressionGrammar.lexical_table, '1+1')
parser = Aurum::Engine::Parser.new(ExpressionGrammar.parsing_table(:expression))
puts parser.parse(lexer).value
最后最后,给另外一个例子,就是 Martin Fowler Blog上的 HelloParserGenerator系列中所用的语法:
1 require 'aurum'
2
3 Item = Struct.new(:name)
4
5 class Catalog < Aurum::Grammar
6 tokens do
7 ignore enum(" \r\n").one_or_more
8 _item range(?a,?z).one_or_more
9 end
10
11 productions do
12 configuration configuration, item {configuration.value = configuration1.value.merge({item.value.name => item.value})}
13 configuration _ {configuration.value = {}}
14 item 'item', _item {item.value = Item.new(_item.value)}
15 end
16 end
17
18 config = Catalog.parse_configuration(<<EndOfDSL).value
19 item camera
20 item laser
21 EndOfDSL
22
23 puts config['camera'].name
P.S.:本文是根据Aurum0.2.0写成的,你可以从rubyforge的svn上得到它。
P.S.P.S.: 在exmaples目录里有一个更复杂一些的例子,是一个简单的Smalltalk解释器。
《Programming Erlang》第8章后面有一个练习,Ring Benchmark。就是说创建N个进程,把它们组合成环状。然后在这个环上把一条消息在环上传递M圈,然后记录所有的时间。实现起来也挺简单,20行左右吧:
1 -module(ring_benchmark). 2 -export([start/2]). 3 4 start(N, M) -> 5 Pid = create_process(self(), N - 1, M), 6 time(fun() -> Pid ! start, loop(Pid, M) end). 7 8 time(Fun) -> 9 statistics(wall_clock), 10 Fun(), 11 {_,Time} = statistics(wall_clock), 12 io:format("Run : ~w s ~n", [Time/1000]). 13 14 create_process(Pid, 0, _) -> Pid; 15 create_process(Pid, N, M) -> create_process(spawn(fun() -> loop(Pid, M) end), N - 1, M). 16 17 loop(_, 0) -> void; 18 loop(Next, M) -> 19 receive 20 Message -> Next ! Message, 21 loop(Next, M - 1) 22 end. 23 24
有意思是它还有一个第二问,让你用另外一种熟悉的语言实现同样的功能,发送同样多的消息,也把时间记录下来,然后写一篇blog来publish你的结果。其实,大家心知肚明,这种lightweight process啊,message passing concurrency啊都是Erlang的强项,而且实测结果也着实颇为恐怖,一般也就没那闲心拿别的东西来陪衬一把了(Armstrong同学自己实现了一个Java version,效率大约能差到百倍吧)。不过还真有那写不信邪的老大, 用stackless python实现了同样的ring benchmark,发现比erlang还快...后来修改代码去掉io操作,Erlang倒是比stackless python快些,但也只是一些而已。
http://www.dcs.ed.ac.uk/home/stg/fengshui.ps.gz今天早上打开Google Reader就看见这么一篇,内容倒也罢了,不过是bad smell的另一个名字而已,硬要扯上分水也只能算是勉勉强强。不过郁闷的是,竟然是个洋人的手笔,国学不昌实不能不令我辈心忧啊。 p.s. 预计未来6个月口头禅:"你这写当心坏了项目的风水"
http://www.infoq.com/cn/articles/domain-web-testing 应用Selenium进行Web测试往往会存在几个bad smell: 1.大量使用name, id, xpath等页面元素。无论是功能修改、UI重构还是交互性改进都会影响到这些元素,这使得Selenium测试变得非常脆弱。 2.过于细节的页面操作不容易体现出行为的意图,一段时间之后就很难真正把握测试原有的目的了,这使得Selenium测试变得难于维护。 3.对具体数据取值的存在依赖,当个别数据不再合法的时候,测试就会失败,但这样的失败并不能标识功能的缺失,这使得Selenium测试变得脆弱且难以维护。 而这几点直接衍生的结果就是不断地添加新的测试,而极少地去重构、利用原有测试。其实这到也是正常,单元测试测试写多了,也有会有这样的问题。不过比较要命的是,Selenium的执行速度比较慢(相对单元测试),随着测试逐渐的增多,运行时间会逐渐增加到不可忍受的程度。一组意图不明难以维护的Selenium测试,可以很轻松地在每次build的时候杀掉40分钟甚至2个小时的时间,在下就有花2个小时坐在电脑前面等待450个Selenium测试运行通过的悲惨经历。因此合理有效地规划Selenium测试就显得格外的迫切和重要了。而目前比较行之有效的办法,往大了说,可以叫domain based web testing,具体来讲,就是Page Object Pattern。 Page Object Pattern里有四个基本概念:Driver, Page, Navigator和Shortcut。Driver是测试真正的实现机制,比如Selenium,比如Watir,比如HttpUnit。它们懂得如何去真正执行一个web行为,通常包含像click,select,type这样的表示具体行为的方法;Page是对一个具体页面的封装,它们了解页面的结构,知道诸如id, name, class,xpath这类实现细节,并描述用户可以在其上进行何种操作;Navigator则代表了URL,表示一些不经页面操作的直接跳转;最后Shortcut就是helper方法了,需要看具体的需要了。下面来看一个超级简单的例子——测试登录页面。 1. Page Object 假设我们使用一个单独的Login Page进行登录,那么我们可能会将登录的操作封装在一个名为LoginPage的page object里:
1 class LoginPage 2 def initialize driver 3 @driver = driver 4 end 5 6 def login_as user 7 @driver.type 'id= ', user[:name] 8 @driver.type 'xpath= ', user[:password] 9 @driver.click 'name= ' 10 @driver.wait_for_page_to_load 11 end 12 end
login_as是一个具有业务含义的页面行为。在login_as方法中,page object负责通过依靠id,xpath,name等信息完成登录操作。在测试中,我们可以这样来使用这个page object:
1 page = LoginPage.new $selenium 2 page.login_as :name => 'xxx', :password => 'xxx' 3
不过既然用了ruby,总要用一些ruby sugar吧,我们定义一个on方法来表达页面操作的环境:
1 def on page_type, &block 2 page = page_type.new $selenium 3 page.instance_eval &block if block_given? 4 end
之后我们就可以使用page object的类名常量和block描述在某个特定页面上操作了:
1 on LoginPage do 2 login_as :name => 'xxx', :password => 'xxx' 3 end 4
除了行为方法之外,我们还需要在page object上定义一些获取页面信息的方法,比如获取登录页面的欢迎词的方法:
def welcome_message @driver.get_text 'xpath= ' end
这样测试也可表达得更生动一些:
1 on LoginPage do 2 assert_equal 'Welcome!', welcome_message 3 login_as :name => 'xxx', :password => 'xxx' 4 end
当你把所有的页面都用Page Object封装了之后,就有效地分离了测试和页面结构的耦合。在测试中,只需使用诸如login_as, add_product_to_cart这样的业务行为,而不必依靠像id,name这些具体且易变的页面元素了。当这些页面元素发生变化时,只需修改相应的page object就可以了,而原有测试基本不需要太大或太多的改动。 2. Assertation 只有行为还够不成测试,我们还要判断行为结果,并进行一些断言。简单回顾一下上面的例子,会发现还有一些很重要的问题没有解决:我怎么判断登录成功了呢?我如何才能知道真的是处在登录页面了呢?如果我调用下面的代码会怎样呢?
1 $selenium.open url_of_any_page_but_not_login 2 on LoginPage { }
因此我们还需要向page object增加一些断言性方法。至少,每个页面都应该有一个方法用于判断是否真正地达到了这个页面,如果不处在这个页面中的话,就不能进行任何的业务行为。下面修改LoginPage使之包含这样一个方法:
1 LoginPage.class_eval do 2 include Test::Unit::Asseration 3 def visible? 4 @driver.is_text_present( ) && @driver.get_location == ![](http://www.blogjava.net/Images/dot.gif) 5 end 6 end
在visible?方法中,我们通过对一些特定的页面元素(比如URL地址,特定的UI结构或元素)进行判断,从而可以得之是否真正地处在某个页面上。而我们目前表达测试的基本结构是由on方法来完成,我们也就顺理成章地在on方法中增加一个断言,来判断是否真的处在某个页面上,如果不处在这个页面则不进行任何的业务操作:
1 def on page_type, &block 2 page = page_type.new $selenium 3 assert page.visible?, "not on #{page_type}" 4 page.instance_eval &block if block_given? 5 page 6 end 7
这个方法神秘地返回了page对象,这里是一个比较tricky的技巧。实际上,我们只想利用page != nil这个事实来断言页面的流转,比如,下面的代码描述登录成功的页面流转过程:
on LoginPage do assert_equal 'Welcome!', welcome_message login_as :name => 'xxx', :password => 'xxx' end assert on WelcomeRegisteredUserPage
除了这个基本断言之外,我们还可以定义一些业务相关的断言,比如在购物车页面里,我们可以定义一个判断购物车是否为空的断言:
1 def cart_empty? 2 @driver.get_text('xpath= ') == 'Shopping Cart(0)' 3 end
需要注意的是,虽然我们在page object里引入了Test::Unit::Asseration模块,但是并没有在断言方法里使用任何assert*方法。这是因为,概念上来讲page object并不是测试。使之包含一些真正的断言,一则概念混乱,二则容易使page object变成针对某些场景的test helper,不利于以后测试的维护,因此我们往往倾向于将断言方法实现为一个普通的返回值为boolean的方法。 3. Test Data 测试意图的体现不仅仅是在行为的描述上,同样还有测试数据,比如如下两段代码:
1 on LoginPage do 2 login_as :name => 'userA', :password => 'password' 3 end 4 assert on WelcomeRegisteredUserPage 5 6 registered_user = {:name => 'userA', :password => 'password'} 7 on LoginPage do 8 login_as registered_user 9 end 10 assert on WelcomeRegisteredUserPage
测试的是同一个东西,但是显然第二个测试更好的体现了测试意图:使用一个已注册的用户登录,应该进入欢迎页面。我们看这个测试的时候,往往不会关心用户名啊密码啊具体是什么,我们关心它们表达了怎样的测试案例。我们可以通过DataFixture来实现这一点:
1 module DataFixture 2 USER_A = {:name => 'userA', :password => 'password'} 3 USER_B = {:name => 'userB', :password => 'password'} 4 5 def get_user identifier 6 case identifier 7 when :registered then return USER_A 8 when :not_registered then return USER_B 9 end 10 end 11 end
在这里,我们将测试案例和具体数据做了一个对应:userA是注册过的用户,而userB是没注册的用户。当有一天,我们需要将登录用户名改为邮箱的时候,只需要修改DataFixture模块就可以了,而不必修改相应的测试:
1 include DataFixtureDat 2 3 user = get_user :registered 4 on LoginPage do 5 login_as user 6 end 7 assert on WelcomeRegisteredUserPage
当然,在更复杂的测试中,DataFixture同样可以使用真实的数据库或是Rails Fixture来完成这样的对应,但是总体的目的就是使测试和测试数据有效性的耦合分离:
1 def get_user identifier 2 case identifier 3 when :registered then return User.find ' .' 4 end 5 end
4.Navigator 与界面元素类似,URL也是一类易变且难以表达意图的元素,因此我们可以使用Navigator使之与测试解耦。具体做法和Test Data相似,这里就不赘述了,下面是一个例子:
1 navigate_to detail_page_for @product 2 on ProductDetailPage do 3 . 4 end
5. Shortcut 前面我们已经有了一个很好的基础,将Selenium测试与各种脆弱且意图不明的元素分离开了,那么最后shortcut不过是在蛋糕上面最漂亮的奶油罢了——定义具有漂亮语法的helper:
1 def should_login_successfully user 2 on LoginPage do 3 assert_equal 'Welcome!', welcome_message 4 login_as user 5 end 6 assert on WelcomeRegisteredUserPage 7 end
然后是另外一个magic方法:
1 def given identifer 2 words = identifier.to_s.split '_' 3 eval "get_#{words.last} :#{words[0..-2].join '_'}" 4 end
之前的测试就可以被改写为:
def test_should_xxxx should_login_successfully given :registered_user end
这是一种结论性的shortcut描述,我们还可以有更behaviour的写法:
1 def login_on page_type 2 on page_type do 3 assert_equal 'Welcome!', welcome_message 4 login_as @user 5 end 6 end 7 8 def login_successfully 9 on WelcomeRegisteredUserPage 10 end 11 12 def given identifer 13 words = identifier.to_s.split '_' 14 eval "@#{words.last} = get_#{words.last} :#{words[0..-2].join '_'}" 15 end
最后,测试就会变成类似验收条件的样子:
1 def test_should_xxx 2 given :registered_user 3 login_on LoginPage 4 assert login_successfully 5 end
总之shortcut是一个无关好坏,只关乎想象力的东西,尽情挥洒Ruby DSL吧:D 结论 Selenium是一个让人又爱又恨的东西,错误地使用Selenium会给整个敏捷团队的开发节奏带来灾难性的影响。不过值得庆幸的是正确地使用Selenium的原则也是相当的简单: 1.通过将脆弱易变的页面元素和测试分离开,使得页面的变化不会对测试产生太大的影响。 2.明确指定测试数据的意图,不在测试用使用任何具体的数据。 3.尽一切可能,明确地表达出测试的意图,使测试易于理解。 当然,除了遵循这几个基本原则之外,使用page object或其他domain based web testing技术是个不错的选择。它们将会帮助你更容易地控制Selenium测试的规模,更好地平衡覆盖率和执行效率,从而更加有效地交付高质量的Web项目。 鸣谢 此文中涉及的都是我最近三周以来对Selenium测试进行重构时所采用的真实技术。感谢Nick Drew帮助我清晰地划分了Driver, Page, Nagivator和Shortcut的层次关系,它们构成我整个实践的基石;感谢Chris Leishman,在和他pairing programming的过程中,他帮助我锤炼了Ruby DSL;还有Mark Ryall和Abhi,是他们第一次在项目中引入了Test Data Fixture,使得所有人的工作都变得简单起来。
最近很多时间都在用Ruby,逐渐地发现了一件很不爽的事情,就是Ruby的end关键字。block多套几层,很容易就最后一页都是end了...难怪有人说,ruby不过是另一种acceptable Lisp,“最后一页都是括号”的经典标志以另外一种形式复现了...对于Lisp的括号,我还是可以接受的,但是满眼的end,直接让我回忆起10年前冲刺NOI的种种,CPU直接切换到实模式,什么可读啊小粒度方法全都没有了,审美观赤裸地变为短小精悍...最后杀红了眼,一行算出文法定义的所有nullable symbols...
1 while @productions.inject(false) {|c, p| c |= !nullable?(p.nonterminal) && p.symbols.all? {|s| nullable? s} && @nullables << p.nonterminal}
注意1不是行号...这句用的statement modifier, 1是我能想到的最小ruby语句了... p.s. 我现在已经恢复到OO保护模式了...刚才追求短小过了头的同时,发现了ruby bulid-in object的一个陷阱... a = Array.new 5, [] [[],[],[],[],[]] a[0] << 1 [[1],[1],[1],[1],[1]] 想不到华丽的Array直接假设传进去的都是值对象了,好歹您也调个dup啊...
Which Programming Language are You?p.s. 这个可能不准...因为李默同学竟然是Lisp...怎么可能...
刚才和李默同学回忆了一下,发现我自从入行以来做了很多x项目...下面一一列举一下。
1. IEC61970 Metadata: Electricity Power Trading System
当时刚上班,team里有一个Doamin知识很厉害的清华的博士,毕业的论文就是电力市场,而清华又是国家引入IEC61970的五家之一。所以他很超前的把这两个东西结合在一起,做成了一个系统。说实话,刚了解IEC61970的时候,我是相当的震撼的,有赶上那时候MDA风气刚起,IEC61970又是同时MOF(Meta Object Facility)和RDF based,华丽得不行。一下子我就变成了一个MDA guy,一个metadata guy...以至于,在BJUG最初的2年里,MDA/MOF/Metadata成为了主旋律...
2. IEC61970 & CWM(Common Warehouse Metamodel) & Office Plugin : Data Warehouse Integration System
这是迄今为止,我最不愿意回忆的一个项目...因为Office Plugin...动辄蓝屏的遭遇让我心有余悸...这是一个backend是J2EE,frontend是.Net的office插件系统,主要是报表...两边都使用CWM作为数据统一的形式...基本上做到一半我的意志就崩溃了...
3. DB Migration/Refactoring : Jyxpearl
这个项目...是李默同学的私房最爱,从大学一直做了很久,改版无数次...当时没有这么流行的好词,什么DB Migration啊,DB Refactoring啊,那时候我们统称导数据...我导了好多会...基本上线一回导一回...时至今日...李默同学总是不无得意的说:你看,你DB Migration的能力就是我培养的...
4. JMI(Java Metadata Interface) & Eclipse RCP : Multi/Rich Client ERP Product
这个team其实挺华丽的,老栾的产品经理,李默是开发经理,超级资深行业专家(人家实际做过生产科长,MRPII,ERP都是人家玩剩下的)老齐做需求,俺是Architect,还有动物园里的猪Senior Dev,我认识人中美工能力第一交互设计能力第一的米米姐做UI和交互。由于当时看了netbeans和sun的官方JMI实现得太玩具。我们决定从自己的JMI实现开始,系统结构要求多客户端,web,rcp都要...所以是超轻http协议的b/s,c/s。结构还是不错的,过程李默和我当然是敏捷了。似乎一起都超级完美的时候,就是要坏菜的时候...企业事业部解散了...
5. Java Communication & Eclipse RCP : IC Card Reader
上面那个项目解散之后,我跟李默赋闲在家,有不忍心打扰政府,自谋生路找的项目...这个项目要用IC卡读卡器,为了锻炼我们的Eclipse RCP能力,我们决定用eclipse rcp来做。于是问题就出来了...IC卡怎么办?google一把发现天无绝人之路...Java有一个Communication包,可以连接serial port...不过当时tricky的是...我的本子没有串口,我们买了一个串口到usb的转换器...发现根本不能用...于是只好跑到李默家用他华丽的台式机(这厮当年誓言旦旦的说,laptop太慢,一定要用台式机,东借西借搞了个2G RAM SATA[注意,这是伏笔]的机器)。我当时就觉得,Java的这个东西基本就是充数的,貌似完全没有人用过,文档啥的都特少...只能自己摸索。在经历了无数次失败之后,终于成功了。在showcase那天的上午,我最后实验了读卡什么的,都没问题。兴高采烈的把jar拷到优盘上,刚插到usb口上...只见一道闪电...机器黑了...据李默后来分析是主板烧了...我说没事,拿上硬盘,土一点也不影响showcase。李默说...这个...SATA耶...还不流行呢...我绿...此后很长时间,我都怀疑是我跟李默同学范冲,超级项目杀手...
6. RDF, Semantic Web, SparQL : Ontology-Relationship DB Mapping
这是在一家公司做产品,当时我元数据/MDA领域颇有积累...跟这家公司做得类似,就过来负责研发本体到关系数据库的映射...兼带在D2RQ的基础上实现一个SparQL查询语言。怎么样...听上去很华丽吧...到现在我都认为,这个项目是我最有潜力的牛皮,不定那天web x.0了,我也老了,我就可以拉着小朋友的手去吹牛b了"05年我就做semantic web,O/R mapping知道不?Ontology啊,你们啊,sometime too simple"...不过估计这一天还早得很呢
7. Agile Domain Specified Language : Goodhope
这个也是李默同学有份的项目...话里的敏捷DSL实践...不过说实话,也有点X...
我常常听到这样的观点:敏捷软件开发并不是真正的革命性的方法,它所采用的技术大多都是古已有之的。比如迭代,你看很哪本软件工程的教科书上没有提到迭代开发呢?在比如说User Story,看上去也不只不过是Use Case的翻版而已吧!甚至我看RUP也和敏捷方法没有太大的区别吧! 要我说,这些人要么是不真的了解敏捷开发,没有认识到敏捷开发的革命性,只是用外在的形式来把它和其他方法进行了比较。有又或者是实施敏捷方法的时候不彻底,所以四处碰壁以至于搞起了修正主义。最可怕的就是某些大公司,看敏捷火了,总有包装一下,到底还是要卖产品。敏捷软件开发就是一个革命性的方法,只不过它要颠覆的不仅仅是低质量的软件开发方式,更重要的是,它要颠覆软件生产企业和软件的使用企业之间的生产关系!!这一点在敏捷宣言里写得再明白不过了
Customer collaboration over Contract negotiation
敏捷软件开发,就是要以一种更合理的共赢的合作关系,代替以前畸形的采购式的合约关系。为什么合约关系就是畸形的?我们来看看合约双方的处境。
首先软件团队方面承担了过多的风险:业务变化,改代码!!商业抉择转换,改代码!!凭啥你甲方的缘故非要我承担额外的成本?你说我冤不冤?冤!但是人家甲方也冤!!人家花了大把的银子,拿到一堆不能用的软件(你要是硬件人家还能转手卖点钱),就像你要砍树别人给你把铲子,你要种树人家给了你把锯。搁你,你也不愿意。且不说博弈,就算双方都有心把事情做好,按合同来,甲方不干;不按合同来,乙方不干,最后变成“有心杀贼无力回天”,大家一起扯扯皮等二期算了。lose-lose,没有赢家。
那么合作的关系是什么呢?合作的关系就好比你去subway买三明治,面包你自己选,要什么肉你来挑,蔬菜,cheese,酱汁你也自己看着办。技术我来,口味你选。技术失败我负责,口味不合适你负责。你做你的强项我来我的强项,最终大家高高兴兴嘻嘻哈哈不吵不闹,作出一顿可口午餐。这是时候,生产关系变了,我不是你的冷冰冰的供应商,你也不是我邪恶的客户,我们是拴在一根绳子上的蚂蚱。成功是我们的,失败也是我们的。荣辱与共,携手并肩。听着有点耳熟?没错,SaaS。敏捷宣言早就说了,CoC啊。从供应商变成服务商,从服务商变成战略合作伙伴,这是在给软件企业指出路,新的生产关系已经尽在其中了。
如果看不清敏捷的这个根本革命点,以为还是开发方法的小打小闹,那么敏捷根本实施不成。这话一般我不敢说的,程序员自发实施敏捷,只在一种情况下可能成功:大企业的IT部门。再赶上个强力的IT领导,自家人嘛,有什么不好谈的。一来二去,就成功了(看看C3,说白了不就是IT部门和业务部门?)但是,如果是做项目的公司,你营销手段不改变,敏捷就不可能成功。你的客户跟你不是合作关系,你通过敏捷增加质量(符合性质量)的工作就不会被人可,那么就不能成为投资,只能是成本。当成本增加到不可承担的时候,敏捷就不了了之了。为什么好多人说老板没有响应?旧的生产关系下敏捷根本就是负担。
说道这里,说一下以敏捷闻名的ThoughtWorks。其实很多人都以为ThougtWorks只有方法论咨询,没错我们是有方法论咨询,但是也有业务模式咨询,客户业务模式不改变,他怎么能彻底敏捷?这点大家不可不查啊。
Yesterday I found a interesting ruby library —— blinkenlights, which enables you to control the LEDs on your keyboard. I thouhgt it could be a cheap replacement of lava light, so I wrote a ruby script called 'Poor Man's Lava'
#!/usr/local/bin/ruby require 'rss/1.0' require 'rss/2.0' require 'open-uri' require 'rubygems' require 'blinkenlights'
SUCCESS = 'success'
def read_rss source='http://cruisecontrolrb.thoughtworks.com/projects/CruiseControl.rss' content = '' open(source) do |s| content = s.read end rss = RSS::Parser.parse content, false rss.items[0].title.include?(SUCCESS) ? all_ok : alarm end
def all_ok times = 50 BlinkenLights.open { |lights| times.times {lights.random} } end
def alarm times = 50, invertal = 0.0 BlinkenLights.open { |lights| times.times {lights.flash invertal} } end
while true read_rss sleep 5 end
make sure to have sufficient permissions to access the device, or you could simple run it as super user.
通常人们会将User Story和Use Case放在一起比较,虽然二者在形式上具有一定相似性,但是究其本质来说,还是天渊之别的。这一点,专业BA李默同学总结的格外准确:“用户故事是可见的商业价值,而不是功能描述”。想要更好的理解这句话,需要了解什么是好的用户故事。好的用户故事,可用INVEST原则来概括:
I - Independent N - Negotiable V - Valuable E - Estimable S - Small T - Testable
我个人觉得,这个总结虽好,但不免分散注意。要我说,想把握好User Story,只用把握两个就够了Negotiable和Valuable。那么首先要确定什么是Negotiable的。User Story有一个流传广泛的书写形式:
As <role>, I'd like to <action>, so that <benifit/value/goal>.
为了更好的获取story还有很多最佳实践,比如personas, 比如business process modeling,其实这些全是糖衣炮弹,As, I'd like to都是噱头,就是为了把用户忽悠晕了,然后图穷匕现直取商业价值和目标。一旦商业价值确定下来,role, action都是可以negotiable。比如李默之前在文章里举的用户登录的例子,输不输用户明密码?可以商量嘛!是不是只有注册用户可以享受个性服务?可以商量嘛!关键是用户想要什么,至于怎么实现这些到头来都是可以商量的,都是Negotiable。只有客户的商业价值是不能商量的,也没的商量。价值没有了,目标不存在了,这个User Story也就没用了,商量啥?重写一张就好了。
因此user story又有另外一个名称,叫requirement placeholder。就是客户价值的"立此存照"。至于具体需求,那么就到iteration plan meeting上是商量吧,看看当时什么样的形式(功能)才是最符合用户需要。到此,其实大家可以看出来了,user story重点就不再How上,而是在Why上的。有了why,且可Negotiable,把握了精神,你就是按用例来写需求又有何妨涅?
有了valuable和negotiable的想法垫底,在看看基于user story的初步计划制定——也就是有名的prioritization——就容易理解多了。用户根据每张卡的价值,自行比较作出决定,大体场景就跟向神仙许愿一样。
神仙:我可以满足你一个愿望。 我:我要荣华富贵!!! 神仙:哦,荣华富贵,那么要不要爱情涅? 我:恩,这个...那我要忠贞的爱情好了!! 神仙:哦,忠贞的爱情,那么要不要健康平安呢? 我:呃.... repeat 无数次,最终我要了一件过冬的皮猴...
自从到了ThouhgtWorks,我订阅的rss越来越少,很多程度上来说,有了自家的Planet TW,很多相看的blog可以一次性看全了,没啥太大的必要去看其他的了。这不,在订阅了Planet TW后的两年中,我只增订了一家:InfoQ。呵呵,想不到如今中文站也来了 http://www.infoq.com/cn/。不错不错。
pair programing是所有XP实践中争议最大的一个,但窃以为确实XP实施的关键关键实践之一,甚至于,我认为很多XP实施的失败都是由于没有采用pair programming而造成的。 要了解pair为什么重要,就要了解pair的目的在何。当然了,大多数人都知道pair的重点在于知识传递,知识共享,持续走查,降低代码缺陷等等等等。这些都是pair的优点,不过最重要的一点却常常被忽略——pair programing的最直接而又最根本的目的之一在于simple design。 ![](http://www.xprogramming.com/images/circles.jpg) 上图是著名的Ron Jefferies Model,可以看到XP最佳实践被划分成了一个一个的圆圈,而pair, TDD, refactor和simple design位于中心。这并不是说这四个实践就是xp的核心。jefferies model每一圈代表了xp实践过程中的不同关注点,最中心的是dev视角,其次是team视角,最外层是交付/管理视角。每圈上的最佳时间多少都有些紧耦合,放开其他的不讲,我们专门说说dev圈,pair programing, tdd, refactor和simple design。 这四个实践里只有simple design最虚也最重要。有一个问题已经被问过无数次了,“到底多simple的design才叫simple”。我对此也有一个近乎刻板的回答:team里所有人员都能够理解的design就是simple的。一旦立了标准,这四个实践的主从关系就一下子清晰起来——simple design是这四个实践的核心,其他三个实践都是它服务的。 首先做出一个设计,最简单的判断标准就是是否可测,一个不可测的设计基本上可以认为无法实现,于是TDD即是simple design的质量保证又是simple design的直觉验证。 refactor是为了得到好的代码,那么什么是好的代码?simple design!!!这里有人不同意了,有人说只是要易于修改和扩展,可是扩展和修改也要别人看得懂才行啊...simple design是起码的要求嘛。实际上,XP中的refactor就是朝着simple design的方向重构过去的,也就是朝着所有人都能理解的代码refactor过去的。插一句题外话,为啥说好的架构的不是设计出来的呢?因为好的架构至少应该是simple design的,而simple的概念有和人员相关...所以当你极尽能事show off你的pattern知识之后,得到复杂设计根本就不可能是好的架构。时刻紧记,架构是妥协啊... 最后,pair programming是simple design的实际检验!!!因为即便是最复杂的设计,只要是你自己想出来的,你都觉得它简单无比,里面充满了直白且显而易见的理由。可惜不幸的是,我们要的简单,是对team里所有人的简单。如果你的pair不能理解你的设计,那么说明你的设计复杂了;如果你们两个人懂,但是swith pair的时候,换过来的人不懂,说明你的设计复杂了。pair programming(以及他那容易让人忽略的子实践switching pair)就是检验simple design的过程。pair programing + refactor就是时刻保证simple design防止过渡设计反攻倒算的过程。pair programming + refactor + tdd就是团结在以Deming同学built quality in的质量大旗下,坚定地与过渡设计做斗争的过程。据我观察,至没有使用pair programming的团队中,少一半simple design成了口号,而这一半中,至少又有一半最终放弃了xp放弃了敏捷(俺以前带过的团队就有这样的...默哀一下)。深刻的教训啊,我们来高呼一下:"pair programming是检验simple design的唯一标准!"。 最后说一下pair programming经济学,过多的假设我就不讲了。单说一点,有哪一位上班的8小时从来不上msn/yahoo/qq等im?有哪一位上班从来不上论坛/不回贴/不发邮件?以我pair的经验来看,pair programming的过程中,两个人几乎不会用im,几乎不会逛论坛。你不好意思呀,毕竟不是你一个人的机器,毕竟是两个人的时间,毕竟你也不愿意给同事一种懒散的印象吧?收回的这么浪费的时间,至少顶得过另外一个人的工作时间了吧?
想要理解敏捷软件开发为什么好,需要从软件质量讲起。那么软件的质量是什么?这个问题有很多中答案,我们不妨想看看传统质量理论对于质量是如何理解的。教科书上说,在20世纪质量管理的发展历程经历了质量检验、统计质量控制和全面质量管理三个阶段。其中,质量理念也在不断的演变。据说有这么几个阶段:
符合性质量 20世纪40年代,符合性质量概念以符合现行标准的程度作为衡量依据,“符合标准”就是合格的产品质量,符合的程度反映了产品质量的水平。比如说我做一个杯子,没什么特别的要求,也不是我神经质的艺术作品,就是普普通通的一个杯子,那么需要高矮长短,大小胖瘦,等等一干质量属性,我做好了可以拿着质量标准来对比,一眼就可以看出那里出了什么问题。通过是否符合这些标准判断产品具有相应的质量。 那么软件的质量理是不是符合性质量呢?我个人觉得不属于。虽然我们一样可以拿出各种各样的标准,比如故障率,比如bug数等等。但是这些标注都满足确不一定是好的软件,比如我写一个helloworld,虽然他可以没有bug。但是却发挥不了任何的作用。这样的软件就属于“高质量”的废品。正如赵辛梅评价方鸿渐,“你不讨厌,但是毫无用处。”,显然毫无用处的软件不会是真正高质量的软件。
适用性质量 20世纪60年代,适用性质量概念以适合顾客需要的程度作为衡量的依据,从使用的角度定义产品质量,认为质量就是产品的“适用性”。是“产品在使用时能够成功满足用户需要的程度”。质量涉及设计开发、制造、销售、服务等过程,形成了广义的质量概念。适用性质量的例子也很多,比如我买了一件Givenchy西服(我还真买了一件),但是一时又没有特别正是的场合(目前还真没有什么正式的场合),于是我一天四顿牛排(其实只有一顿),于是就吃胖了,这件华丽的Givenchy就穿不上了。那么这件衣服从符合性质量来说,是优质品,但是从适用性质量来说,却不是一个高质量的产品——因为我穿不上。还有一句话,叫甲之熊掌乙之砒霜。也是适用性质量的标准体现。 那么软件的质量是不是适用性质量呢?我个人觉得,软件的质量至少是适用性质量。软件,尤其是定制软件/企业软件,就是量体裁衣。软件的基本质量就是要在用户使用的过程中发挥价值,支撑客户的业务发展。 书上说,从“符合性”到“适用性”,反映了人们在对质量的认识过程中,已经开始把顾客需求放在首要位置。但是它没说怎么才能做到把客户需求放到首要位置。我看光靠文档是堆不出来的,光考说说也是不行的。这个后面讲,戴明同学比我讲得好。
满意性质量 20世纪80年代,质量管理进入到TQM阶段,将质量定义为“一组固有特性满足要求的程度”。它不仅包括符合标准的要求,而且以顾客及其他相关方满意为衡量依据,体现“以顾客为关注焦点”的原则。这个的最典型的例子是麦当劳,他所有的店铺从风格到食物都保持在同一水平,使你无论在那里,都可以得到一定的购物体验。也就构成了对麦当劳的满意性质量的验证。这个软件上也是有例子的,内举不必亲,ThoughtWorks大多数项目都可以达到“满意性质量”,呵呵谁让俺们是consultant涅。 我隐约觉得满意性质量应该是一个过程的质量,而不仅仅是软件的质量,但是目前没有好的想法,暂且按下不表。
卓越质量 ......下略100字。个人觉得大多数软件还没有达到适用性质量,大多是过程也都没有达到满意性质量,卓越质量就先不说了吧。
总之,我们大体的认为软件质量主要是适用性质量起码是不会错的。那么怎么才能达到这个质量标准涅?俺是做软件的,质量管理还是看看Deming同学怎么说吧,不过他老人家的14点总是发生变化。我也只好断章取义,说说一个敏捷开发人员眼中的14原则:
1. 持之以恒地改进产品和服务 Create constancy of purpose for improvement of product and service
这个很明显嘛,small release,快速发布,每次发布都是对产品的持续改进。
2.采用新的观念 Adopt the new philosophy
敏捷啊...
3.停止依靠大规模检查去获得质量 Cease dependence on mass inspection
这个还有另一个说法,build quality in。TDD,QA/BA全程参与,都是build quality in的好方法。
4.结束只以价格为基础的采购习惯 End the practice of awarding business on the basis of price tag alone
这个...貌似是说请咨询吧...
5.持之以恒地改进生产和服务系统 Improve constantly and forever the system of production and service
这个是敏捷过程的持续改进,对应的实践大家可能比较陌生——Restrospective!!!
6.实行岗位职能培训 Institute training on the job
Pair Programming,Learning Lunch敏捷从来都不缺乏学习的机会,就看你有没有学习的动力了。
7. 建立领导力企业管理 Institute leadership
敏捷团队的终极目标,自组织团队,的管理是也。
8. 排除恐惧 Drive out fear
XP第一原则,勇气,不要恐惧。
9. 打破部门之间的障碍 Break down barriers between staff areas
只有开发团队的敏捷不是真正的敏捷,敏捷说到底,是将软件的供求关系从合约型转为合作型,本来就要是大破障碍。而且障碍不打破,就很难将敏捷实施到底。这也是很多同学尝试敏捷失败的原因,仅仅以为敏捷是技术层面上的事情,其实不是。从这个角度来所,敏捷方法的确是深刻而震撼心灵的变革,有些人...呃...敏捷在十月...
10. 取消对员工的标语训词和告诫 Eliminate slogans, exhortations, and targets for the work force
恩,什么激情100天...封闭开发...见鬼去吧...不过restrospective的结果是要写在白板上的,准备时刻改进。自我表扬和自我批评,算不上训词吧。
11.取消定额管理和目标管理 Eliminate numerical quotas for the work force. Eliminate management by objectives
很多人都问过我,pair programming了之后,技校怎么办?嘿嘿,Deming同学已经说了,这样的考核不要也罢。
12 消除打击员工工作情感的考评 Remove barriers that rob the hourly worker of his right to pride of workmanship. Remove barriers that rod people in management and in engineering of their right to pride of workmanship
敏捷团队的自我评价很简单,360度,由于你几乎跟所有人都pair过,如果所有人都不说你好...这已经是rp问题了,就不是打击这么简单了...
13 鼓励学习和自我提高 Encourage education and self-improvement for everyone
同前,Pair Programming,Learning Lunch敏捷从来都不缺乏学习的机会,就看你有没有学习的动力了。
14 采取行动实现转变 Take action to accomplish the transformation
每次restrospective之后必须定出方案,以实践改进。而诸位如果想实施敏捷又觉得难于说服领带,不妨拿Deming同学说说事,这位大老的杀伤力还是曼大的,尤其你老大是MBA的话
很长时间以来我对rails框架本身没什么兴趣,因为我从来都不是web fans,15岁那年我打印了一本HTML Reference准备学习一下,感觉完全nonsense。很长一段时间(大约有3年吧)里基本上看到Markup Language我就会大脑短路。但是我对rails背后的东西很感兴趣,是什么让rails如此有效率,一开始我以为是Ruby DSL,但随着做的rails项目越来越多,我发现rails的效率的根源来自它对delphi的继承和发扬。 1. data centric object model Active Record delphi之所以成功,在于它看准了大部分商用软件都是数据库核心的,并为之设计一套相应的框架, delphi的核心就是围绕数据库进行开发(李维同学的borland传奇里写道,当时之所以起名字叫Delphi,就是因为要taking to Oracle)。而rails也很好的在新时代(Web时代)把握了相似的核心点——content是web的核心,以及数据库数据到content的映射是动态web的核心。因此它没有采用所谓的更加严肃的ORM技术。而是依然使用了delphi时代的active record(但是进行了object封装)。如下代码示范了ruby和delphi在active record实现上的相似 Ruby class Person < ActiveRecord::Base end
x8x = Person.new :name => 'x8x' x8x.age = 15 Delphi people : TADOTable;
![](/Images/dot.gif) begin people.Table = 'people'; people.InsertRecord('x8x');
people.First; people.FieldByName('age') := 15; end 可以看出,Delphi的数据库味道更浓一些,而ruby则更多使用了对象的视角(我记得在98年前后(我变成oo狂热分子之后),在写delphi的时候我基本上不会直接使用Table对象了,而是做一些简单的封装,用business object代替直接的数据库对象。但是最后由于和delphi的ui组件结合的不好而放弃)。但是active record的pattern是一致的。 2. DB(resource)-aware UI component —— Action View Delphi另一个为人称道的地方是DB-Aware的UI组件,像TDBLabel, TDBMemo还有至今仍位人称道的TDBGrid,极大的简化了UI的开发。不过说到底,仍然是Delphi数据库核心策略的延续。同样,rails的view helper也是db核心的。text_field之类的可以自动感知active record的内容和错误。 <label>Name:</label> <%= text_field 'person', 'name' %> 和 nameLabel : TDBLabel;
nameLabel.DataSource = peopleTable; nameLabel.Field = 'name'; nameLabel.Label = 'Name';
抛开Desktop和web的差异,也可以算是大致相当吧。 3. Simple Component Model —— Plan Object as Component Delphi是基于组件开发,并且是非常成功的一个组件模型。基本上有经验的delphi程序员,都会在开发过程中抽象几个VCL component出来简化自己的开发。一方面是DRY精神,另一方面Delphi简单的组件模型使得这样做的代价非常的小。Delphi的组件基本上就是一个对象,重构的过程中修修改改就成组件了。rails其实有类似的机制,而且更简单直接,更符合web时代的胃口,一个对象外加一个helper就可以成为一个UI组件。与此同时rails还有另外一个天然的同盟——Ruby DSL。把以前Delphi需要用UI设计器的地方都用Ruby DSL代替了。这一点是我最近在用rails做曹老师华丽的RedSaga的时候推行DSL geek主义时发现的。比如现在我有这样一个tiny DSL用以定义portlet: in controller @portlet = Portlet.new do name 'administration' title 'Administration' tabs do Projects :controller =>'projects', :action => 'list' end end in view <%= render_portlet @portlet %> 这种描述/configuration block风格的dsl与delphi组件的初始化非常相似,或者可以说,只有语法上的差异而无思路上的差异(当然delphi可以借助IDE而不是语言来指定这些,但是这个做法是没有生产力的)。 with Portlet do Label = ![](/Images/dot.gif) Name = . Tabs[0].Controller = ![](/Images/dot.gif) Tabs[1].Action = ![](/Images/dot.gif) end rails和delphi这种轻量的组件模型,使得构建组件/复用组件级的代价极小。因此可以极大的提高开发效率(我至今仍记得,01年前后接了一个Delphi私活,客户要求Office XP菜单风格,我找了一个XPMenu的控件,直接仍上去,自己的代码一行没改,菜单就Office XP了...)。 总之,Delphi的效率要素Rails大部分都学走了,最后简单总结一下rails在delphi基础上的发扬: 1. 用ruby而不是object pascal。语法上更灵活简单 2. Object Model on top of ActiveRecord,比起Delphi可以跟好的使用OO开发。 3. 组件模型更简单 4. CoC,这个就不说了 5. expressive Ruby DSL 最后最后,说一下Delphi for PHP,做得很华丽。但是UI部分我不是很喜欢。
I got a Sony PSP recently. After playing with it for a while, I
found out that, comparing to other handheld game console, PSP has a
fairly open platform. Some great guys had already customized a gcc
compiler for PSP, and also, a simple toolchain is provided by the
active community to support porting/development/debuging/hacking. So I
decided to port some of my favourite programming langauges to PSP. Of
course, the first one on my list is Ruby.
Cross compiling,
reading psp documents, reading ruby source code, hacking ruby source
code, cross compiling... after 3 days hard working, finally, I managed
to make the following ruby script(which is listed in the book
<Programming Ruby>) running on my PSP:
def say_goodnight(name)
"Good night, #{name}"
end
puts say_goodnight("PSP")
Bingo~~~! Ruby goes entertainment!!!!
btw : my ruby-psp patch could be found here:
https://rubyforge.org/tracker/index.php?func=detail&aid=8134&group_id=426&atid=1700
make sure you have psp-gcc and toolchain installed on you PC, and 3.03-OE/1.5 fireware on your PSP.
今天被老庄拉到JavaEye扯皮,扯来扯去还是lambda演算...本来应承了老庄写lambda演算简介,不过看到磐石T1同学提到了Church number来勾引whl同学...于是我想还是写一些更有意思的东西吧。 每个Church number都是一个接受两个参数的函数,这两个参数又都是函数,第一个参数称为后继函数,第二个参数则叫做零点函数。依据这两个函数,我们可以定义Church number zero, one, two: (define zero (lambda (successor zero) zero)) (define one (lambda (successor zero) (successor zero))) (define two (lambda (successor zero) (successor (successor zero))))
可以看出,所谓one就是对零点函数应用一次后继函数,而two则是对零点函数应用后继函数的结果再次应用后继函数,依次类推可以得到Church Number n。下面我们可以通过后继函数increase和零点函数f(x) = 0来看看这些Church Number的计算结果: (define (increase x) (+ x 1))
(zero increase 0) > 0 (one increase 0) >1 (two increase 0) >2
an approximate Java version:
public interface Function<T> { T apply(Object... parameters); }
public interface ChurchNumber { Integer apply(Function<Integer> successor, Function<Integer> zero); }
ChurchNumber zero = new ChurchNumber() { public Integer apply(Function<Integer> successor, Function<Integer> zero) { return zero.apply(); } };
ChurchNumber one = new ChurchNumber() { public Integer apply(Function<Integer> successor, Function<Integer> zero) { return successor.apply(zero); } };
ChurchNumber two = new ChurchNumber() { public Integer apply(Function<Integer> successor, Function<Integer> zero) { return successor.apply(successor.apply(zero)); } };
Function increase = new Function<Integer>() { public Integer apply(Object... parameters) { if (parameters[0] instanceof Function) { return ((Function<Integer>) parameters[0]).apply() + 1; } return (Integer) parameters[0] + 1; } };
Function numberZero = new Function<Integer>() { public Integer apply(Object... parameters) { return 0;} };
System.out.println(zero.apply(increase, numberZero)); >0 System.out.println(one.apply(increase, numberZero)); >1 System.out.println(two.apply(increase, numberZero)); >2
定义了Church number后,我们继续定义Church number上的运算,首先是增加1: (define (inc x) (lambda (successor zero) (successor (x successor zero))))
(define three (inc two)) (three increase 0) >3
an approximate Java version:
static ChurchNumber inc(final ChurchNumber churchNumber) { return new ChurchNumber() { public Integer apply(Function<Integer> successor, Function<Integer> zero) { return successor.apply(churchNumber.apply(successor, zero)); } }; }
ChurchNumber three = inc(two); System.out.println(three.apply(increase, numberZero)); >3
然后是加法: (define (add x y) (lambda (successor zero) (x successor (y successor zero))))
(define five (add three two)) (five increase 0) >5
an approximate Java version:
static ChurchNumber add(final ChurchNumber x, final ChurchNumber y) { return new ChurchNumber() { public Integer apply(final Function<Integer> successor, final Function<Integer> zero) { return x.apply(successor, new Function<Integer>() { public Integer apply(Object... parameters) { return y.apply(successor, zero); } }); } }; }
ChurchNumber five = add(two, three); System.out.println(five.apply(increase, numberZero)); >5
最后是乘法: (define (multiply x y) (lambda (successor zero) (x (lambda (z) (y successor z)) zero)))
(define four (multiply two two)) (four increase 0) >4
an approximate Java version:
static ChurchNumber multiply(final ChurchNumber x, final ChurchNumber y) { return new ChurchNumber() { public Integer apply(final Function<Integer> successor, Function<Integer> zero) { return x.apply(new Function<Integer>() { public Integer apply(final Object... parameters) { return y.apply(successor, new Function<Integer>() { public Integer apply(Object... ignoredParameters) { if (parameters[0] instanceof Function) { return ((Function<Integer>) parameters[0]).apply(); } return (Integer) parameters[0]; } }); } }, zero); } }; }
ChurchNumber four = multiply(two, two); System.out.println(four.apply(increase, numberZero));
没有减法和除法,Church当年发明这套东西的时候就没有。原因是非常明显的...因此Church number只有后继函数,而没有前驱函数。也就是说Church number只能往前数...不能望后数...自然不可能作出减法和除法了。当然扩展一下也是非常容易的: (define negative-one (lambda (successor precursor zero) (precursor zero))) (define one (lambda (successor precursor zero) (successor zero)))
(define (add x y) (lambda (successor precursor zero) (x successor precursor ( y successor precursor zero) )))
(define (inc x) (+ x 1)) (define (dec x) (- x 1))
(define zero (add one negative-one))
(zero inc dec 0) >0
whl同学问这样能不能实现浮点,答案是可以实现有限精度的浮点数....因为按照这个思路发展下去,我们定义浮点的successor和precursor函数只能在有限的位数之内...当然有了one,zero再结合pair,模拟0/1存储实现浮点也不是不可能的事情...
1. Never use Selenium FIT mode
Selenium分为两种运行模式,Driven Mode(现在叫Selenium Remote Control)和FIT Mode(现在叫Selenium Core)。
FIT Mode顾名思义,就是类似FIT Testing Framework那种使用方式,主要用于QA等非技术人员编写Web应用的功能测试。FIT Mode的Selenium测试使用HTML来组织测试用例。例如我要测试一个web应用的登陆功能。我可能写出这样的HTML 表格。
1
<
table
>
2
<
tr
>
3
<
td
>
open
</
td
>
4
<
td
>
http://localhost:8080/login
</
td
>
5
<
td
></
td
>
6
</
tr
>
7
<
tr
>
8
<
td
>
type
</
td
>
9
<
td
>
id=username
</
td
>
10
<
td
>
someuser
</
td
>
11
</
tr
>
12
<
tr
>
13
<
td
>
type
</
td
>
14
<
td
>
id=password
</
td
>
15
<
td
>
password
</
td
>
16
</
tr
>
17
<
tr
>
18
<
td
>
click
</
td
>
19
<
td
>
id=login_button
</
td
>
20
<
td
></
td
>
21
</
tr
>
22
<
tr
>
23
<
td
>
assertTextPresent
</
td
>
24
<
td
>
Welcome to xxxx
</
td
>
25
<
td
></
td
>
26
</
tr
>
27
</
table
>
不同于FIT,Selenium内置了一系列的命令,如上例中的open, type, click以及assertTextPresent,因此QA可以完全抛开DEV独立地编写测试(FIT需要DEV提供Behavior Fixture)。因此FIT Mode是相当容易使用的,哪怕不会使用HTML的QA,也可以使用FrontPage画出三列表格,依次填入数据。
然而对于大多数team而言——尤其是敏捷team,FIT Mode平易的外表下是令人恐惧的泥沼。大多数团队往往选择使用Selenium作为功能测试和集成测试工具而不仅仅是QA测试工具,在不同的迭代间遇到功能流程或UI变化时,必须要重构Selenium测试,或者说,Functional Test Migration。令人遗憾的是,HTML based的Selenium FIT Testing的重构竟然令人难以置信的困难。我们可以使用include等Selenium FIT扩展,使得它可以重用详细的功能(Log in, Log out诸如此类)。即便如此,在一个真实的项目中,Selenium Test的数量往往在200-500之间(我目前所处的项目在改用Driven Mode前已达350+),对于这么大基数的Selenium测试,手工重构几乎是不可想象的,而目前尚没有HTML代码重构工具。即便存在泛泛意义上的HTML重构工具,对于Selenium测试重构的有效性尚待商榷。而使用Driven Mode上述代码可以写为:
1
public
void
testShouldShowAWeclomeMessageAfterUserLoggedIn()
{
2
selenium.open(
"
http://localhost:8080/login
"
);
3
selenium.type(
"
id=username
"
,
"
someuser
"
);
4
selenium.type(
"
id=password
"
,
"
password
"
);
5
selenium.click(
"
id=login_button
"
);
6
assertTrue(selenium.isTextPresent(
"
Welcome to xxxx
"
));
7
}
很自然,一个训练有素的程序员会重构出如下代码:
1
public
void
login(String username, String password)
{
2
selenium.open(
"
http://localhost:8080/login
"
);
3
selenium.type(
"
id=username
"
,username);
4
selenium.type(
"
id=password
"
, password);
5
selenium.click(
"
id=login_button
"
);
6
}
7
8
public
void
testShouldShowAWeclomeMessageAfterUserLoggedIn()
{
9
login(
"
someuser
"
,
"
password
"
);
10
assertTrue(selenium.isTextPresent(
"
Welcome to xxxx
"
));
11
}
之后无论是pull up到公共基类还是extact到Utils class都是很容易的事情。由于Java在代码重构上便利,Java Selenium Remote Control成为使用Selenium的最佳方式。在这一点上,纵使Ruby语法上比Java简单灵活得多,它仍不是编写Selenium测试的最佳载体(当然一个经过精心设计的ruby selenium dsl wrapper还是具有非凡的价值的,这个我们后面会涉及到)。
2. Using the name user, system, page instead of selenium
观察上面提到的代码,其中使用selenium来操纵web应用的行为,这在Remote Control里是常见的做法,但是仍然不够好,我们可以做一些小的变化以得到更好的测试:
1
protected
void
setup()
{
2
selenium
=
![](/Images/dot.gif)
//
intialize selenium instance
3
user
=
selenium;
4
currentPage
=
selenium;
5
}
6
7
public
void
login(String username, String password)
{
8
user.open(
"
http://localhost:8080/login
"
);
9
user.type(
"
id=username
"
,username);
10
user.type(
"
id=password
"
, password);
11
user.click(
"
id=login_button
"
);
12
}
13
14
public
void
testShouldShowAWeclomeMessageAfterUserLoggedIn()
{
15
login(
"
some guy
"
,
"
password
"
);
16
assertTrue(currentPage.isTextPresent(
"
Welcome to xxxx
"
));
17
}
基本上这只不过是"另一种写法"而已,但是它更好的表达了"用户的行为",如login代码所示。以及"系统的正确相应",即currentPage.isTextPresent()。这种是典型的对编译器无意义对人有意义的代码,也就是普遍意义上好的代码。
3. Creating a DSL base on your test codes
懂得HTML的QA可以在没有DEV的帮助下使用Selenium FIT mode,然而却不能在没有DEV的帮助下使用Driven Mode。于是最自然也是最fashion的做法,就是在已有的test codes之上提供Testing DSL或者Scripting Language,让FIT mode变得更加FIT。这方面内容是一个更大的主题,以后再详细展开吧。
4. Hacking Selenium Object to support FIT command
Selenium FIT mode和RC mode下的命令有些许差异,比如FIT中的assertTextPresent,在RC中变成了isTextPresent。同样还有FIT中最实用的命令clickAndWait,在RC中变成了click和waitForPageToLoad。在RC中使用FIT mode中的命令也非难事,找到com.thoughtworks.selenium.Selenium,添加方法:
public
void
doCommand(String commmand, String parameters);
然后在com.thoughtworks.selenium.DefaultSelenium中添加实现:
1
public
void
doCommand(String commmand, String parameters)
{
2
String[] paras
=
new
String[]
{
""
,
""
,
""
}
3
for
(
int
i
=
0
; i
<
parameters.length
&&
i
<
3
; i
++
)
4
paras[i]
=
parameters[i];
5
commandProcessor.doCommand(command, paras);
6
}
然后试验一下:
selenium.doCommand(
"
clickAndWait
"
);
在我们使用纯RC mode之前曾经用过一段中间方案,将rc code转化为fit code来跑(因为rc不支持https),由于不是真正的rc mode,像isTextPresent之类的方法都没有办法使用,只能使用FIT mode command。因此如果因为一些特殊的原因(https, chrome起不来,hta bug多等等),你没有办法使用RC mode,但是有希望得到RC可重构的好处,那么这个tricky的技巧倒是不错的选择。
5. Using chrome and IE hta lanucher to support https 6. Run test using different browser lanucher to test browser compatibility
这两个都是和browser lanucher相关的,Selenium和JWebUnit最大的不同在于它使用真实的浏览器来跑测试,从而可以更加真实地考察系统在不同浏览器中的表现。因此使用不同的浏览器lanucher来运行测试,可以更好测试应用的浏览器兼容性,这对于web 2.0应用而言是很有帮助的。此外,使用rc提供的试验性lanucher,chrome和hta可以解决跨domain测试和https的问题。不过目前hta还是有很多bug的,推荐使用chrome。当然,最希望的还是澳洲的同事可以早日在selenium里提供https支持。
摘要: 参加ThoughtWorks University的一个来月没啥事情,闲了写写compiler玩。发现Lexer部分比较基础也比较常用,有很多相似的东西,每次都要写一遍也太麻烦了,下面是我按着JSL写的一个common java-like lexer,对于大多数接近java语法的语言估计是够用了。BTW:这个Lexer定义是TDD出来,以通过测试为要务,可能可读性不太强。1.WhiteSpaceC... 阅读全文
Watir doesn't work well with chinese characters. Try the following codes. ie.text_field(:name, 'some text field).set('某某') It will highlight the text field but put nothing in it. I read the Watir source codes, and found an interesting code segment: 1 for i in 0 .. value.length-1 2 sleep @ieController.typingspeed # typing speed 3 c = value[i,1] 4 #@ieController.log " adding c.chr " + c #.chr.to_s 5 @o.value = @o.value.to_s + c #c.chr 6 fire_key_events 7 end The above codes show how Watir simulates typing.If it doesn't work well with chinese characters, There must be something wrong with Ruby string. The first order of business is to figure out how Ruby string works for Chinese string. 1 chineseString = '某某' 2 puts chineseString.length 3 for i in 0..chineseString.length-1 4 puts chineseString[i, 1] 5 end result will be: 4
Does Ruby, which is now capturing all java programmers' love, use 8bit char instead of unicode? Holy fuck! I made a simple patch for the issue after I woke up from a short coma. 1 require 'Watir' 2 3 module Watir 4 module Cn 5 class IE <Watir::IE 6 def text_field(how , what=nil) 7 return TextField.new(self, how, what) 8 end 9 end 10 11 class TextField < Watir::TextField 12 def doKeyPress( value ) 13 begin 14 maxLength = @o.maxLength 15 if value.length > maxLength 16 value = suppliedValue[0 .. maxLength ] 17 @ieController.log " Supplied string is #{suppliedValue.length} chars, which exceeds the max length (#{maxLength}) of the field. Using value: #{value}" 18 end 19 rescue 20 # probably a text area - so it doesnt have a max Length 21 maxLength = -1 22 end 23 24 Cn.characters_in(value) {|c| 25 sleep @ieController.typingspeed 26 @o.value = @o.value.to_s + c 27 fire_key_events} 28 end 29 end 30 31 def Cn.characters_in(value) 32 index = 0 33 while index < value.length 34 len = value[index] > 128 ? 2 : 1 35 yield value[index, len] 36 index += len 37 end 38 end 39 end 40 end I submitted this patch to Watir tracing systsem,and zipped codes could be found here: http://rubyforge.org/tracker/index.php?func=detail&aid=3232&group_id=104&atid=489
昨天发现了Ruby Watir里的一个小问题,没有办法在Text Field里输入中文。虽然已经hack了,但是还是不太爽,G.H.Hardy说: Beauty is the first
test: there is no permanent place in this world for ugly mathematics. 感动了我好久。现在换个说法: Beauty is the first
test: there is no permanent place in this world for ugly hack code. 这个问题也不太难出理,ruby作为C的front interface在字符串处理上有很深的C的痕迹,嗯,10年前我还是个C程序员嘛,按照从前的做法区分ASCII码。 1 def characters_in(value) 2 index = 0 3 while index < value.length 4 len = value[index] > 128 ? 2 : 1 5 yield value[index, len] 6 index += len 7 end 8 end 把TextField里的doKeyPress改一下: 1 characters_in(value) {|c| 2 sleep @ieController.typingspeed 3 @o.value = @o.value.to_s + c 4 fire_key_events} 搞定!但是还是很丑,直接把别人的code改了,contributing to eclipse里说要contribute不要随便change别人的代码。好吧,好在ruby扩展起来也不难: 1 require 'Watir' 2 3 module Watir 4 module Cn 5 class IE <Watir::IE 6 def text_field(how , what=nil) 7 return TextField.new(self, how, what) 8 end 9 end 10 11 class TextField < Watir::TextField 12 def doKeyPress( value ) 13 begin 14 maxLength = @o.maxLength 15 if value.length > maxLength 16 value = suppliedValue[0 .. maxLength ] 17 @ieController.log " Supplied string is #{suppliedValue.length} chars, which exceeds the max length (#{maxLength}) of the field. Using value: #{value}" 18 end 19 rescue 20 # probably a text area - so it doesnt have a max Length 21 maxLength = -1 22 end 23 24 Cn.characters_in(value) {|c| 25 sleep @ieController.typingspeed 26 @o.value = @o.value.to_s + c 27 fire_key_events} 28 end 29 end 30 31 def Cn.characters_in(value) 32 index = 0 33 while index < value.length 34 len = value[index] > 128 ? 2 : 1 35 yield value[index, len] 36 index += len 37 end 38 end 39 end 40 end 测试一下: require 'watir-cn'
ie = Watir::Cn::IE.start('http://www.google.com') ie.text_field(:name, 'q').set('Ruby Watir 功能测试' 成功。最后一步是贡献社区,直接登到rubyforge,找到Watir然后submit了两个patch:一个直接修改watir库的一个是独立的watir-cn的。推荐大家使用第二个的patch。地址在: http://rubyforge.org/tracker/index.php?func=detail&aid=3232&group_id=104&atid=489
今天我和WPC被迫派给了一个很nonsense的活,客户给了我们两份名单,让我们对照我们SSO中的用户数据做一下数据维护,如果有的用户在SSO中没有就加进来;要是SSO中有,看一下OA里有没有,如果OA里没有写一个列表让OA的同志们去维护数据。本来是一个很枯燥的活,好在WPC和我都是pragmatic programers,于是生活变得有乐趣多了。 解决这个问题最直接的做法,就是login到SSO平台上,然后一个用户一个用户的search,search完了再用OA Admin登陆查OA帐户。我们是pragmatic programmer嘛,这么繁琐的活动自然写程序搞定它。自然浮现两个选择:Ruby Watri,还有就是产自俺们公司的Selenium Script。 上来先用Ruby Watri,这个东西好啊,简单啊。WPC找了一个以前写的example, 我照着改了一个用户的search,然后扩展:
1 # login in as sso admin 2 ie = Watir::IE.start(sso_login_url) 3 ie.text_field(:name,"username").set(sso_admin_user) 4 ie.text_field(:name,"password").set(sso_admin_pass) 5 ie.button(:value, "登录").click 6 7 # search user 8 ie = Watir::IE.start(sso_search_url) 9 ie.text_field(:name, "userName).set('张三') 10 ie.button(:value, "查找").click 跑到command line run一把,ruby login.rb,然后一个古怪的事情出现了
ie.text_field(:name, "userName).set('张三') userName输入框highlight了一下,然后没有字...难道是编码问题?换了encoding重新save了一把,结果一样。郁闷...于是我和WPC想法是...Ruby中文有问题,不管了时间紧迫,换Selenium Test,自家的东西嘛。但是Selenium的Script是HTML-based的,写起来太麻烦。我们是pragmatic programmer嘛,这么繁琐的活动自然写程序搞定它。于是我来搞一个ScriptGenerator,WPC同志搞script template。一搞template WPC同志就比较兴奋,大喊:velocity! velocity!哎,我机器上没有velocity的library,于是我决定pragmatic一把,直接writer output。找了一个Selenume Script Demo,在每行前面加上aaaa,每行末尾加上bbb,然后ctrl + f,aaa->writer.write(" bbb->"); 改几个",introduce parameter, extract method, compose method飞快地重构之,一个hard code的generator引擎诞生了。WPC还在调template,我看了一下代码,蛮ugly的,refactory之:
1 private static String scriptTemplate; 2 3 public static void readScriptTemplate(String templateName) { 4 BufferedReader reader = null; 5 try { 6 reader = new BufferedReader(new FileReader(templateName)); 7 String line = null; 8 StringBuffer template = new StringBuffer(); 9 while ((line = reader.readLine()) != null) 10 template.append(line).append("\n"); 11 scriptTemplate = template.toString(); 12 } catch (IOException e) { 13 14 } finally { 15 if (reader != null) 16 try { 17 reader.close(); 18 } catch (IOException e) { 19 } 20 } 21 } 22 23 public static void generatedScriptForUser(String path, String name) { 24 Writer writer = null; 25 try { 26 writer = new BufferedWriter(new FileWriter(path + "/" + name 27 + ".html")); 28 writer.write(scriptTemplate 29 .replaceAll("\\$\\$userName\\$\\$", name)); 30 } catch (IOException e) { 31 e.printStackTrace(); 32 } finally { 33 if (writer != null) 34 try { 35 writer.close(); 36 } catch (IOException e) { 37 } 38 } 39 40 } 一下子少了无数代码,爽多了。然后wpc也搞好了template,按模版文件一generating,几十个selenium test script就出现了。嗯,write program that write program,有够pragmatic。 写了一个Test Suite,放到改一下Selenium Runner下跑一下又傻眼了。Selenium做的Functional Test,一般假定和被测的应用在一个URL base里,因此这样local“测”remoting就不太好,而且我们又是一个安全平台,URL base做安全基准的。一下就所有测试就crackdown在这里了。郁闷啊... Selenium文档,发现可以用driver来adpater local和remoting的环境,问题是这个drvier要自己写...郁闷... WPC在firefox上装了一个Selenium Recorder的plug in可以记录web page actions,然后replay。他发现这个东西能绕过Selenium的限制,于是决定看看他怎么实现的,找到code一看...原来是把selenium runner hack了...用javascript把url base生生的给改了...WPC说好啊,我们写一个Firefox Selenium Recorder Plugin的plug in吧,让他从一个目录里自动load script...然后WPC开始玩firefox plugin. 我决得我还是看看Ruby的中文支持吧,找来找去都没有说Ruby的中文有问题的,后来发现一个老大测了一下Ruby的中文字符串,说没问题。我忘了这个老大的URL了找到再补上。代码上看起来似乎也没什么问题。于是我怀疑是Watri的问题,看Watri的代码,发现Watri的设计思路就是为了模拟人的录入,然后找到这样的代码:
def doKeyPress(value)
![](/Images/dot.gif)
for i in 0 .. value.length-1 sleep @ieController.typingspeed # typing speed c = value[i,1] #@ieController.log " adding c.chr " + c #.chr.to_s @o.value = @o.value.to_s + c #c.chr fire_key_events end
![](/Images/dot.gif)
end 根据设定的延时模拟人敲击键盘。每一个间隔用String slice来输入。于是我试验了一下ruby的中文字符串切片:
1 value = "哈哈" 2 for i in 0..value.length-1 3 puts value[i,1] 4 end Ruby果然瞎菜了...value.length是4,每一个切片都是空...啊~~~~天啊,8bit char...C的时代啊。找到了问题就好办了,我权衡了fix watri unicode和直接hack它,最后我选择直接hack它,方法简单:
1 if @ieController.typingspeed != -1 2 for i in 0 .. value.length-1 3 sleep @ieController.typingspeed # typing speed 4 c = value[i,1] 5 #@ieController.log " adding c.chr " + c #.chr.to_s 6 @o.value = @o.value.to_s + c #c.chr 7 fire_key_events 8 end 9 else 10 @o.value = value 11 fire_key_events 12 end 然后测试一下:
1 require 'Watir' 2 3 ie = Watir::IE.start("http://www.google.com") 4 ie.typingspeed = -1 5 ie.text_field(:name, "q").set("哈哈") 搞定。于是准备改Ruby脚本,这个时候客户下班了,我们决定明天继续,一共用时2小时... 最后说一下需求,一共有多少数据呢?70条...如果pair录入的话,大约40-50分钟吧 结论: 1.Pragmatic Programmer都是很懒的,重复5次的工作都回用代码来写。 2.Pragmatic Programmer都是很有好奇心的,太多的重复性劳动只能分散他们的注意力,不知道会搞出什么了,我估计我要没有hack Watri,WPC已经开始写Firefox plugin了。 3.Pragmatic Programmer都是“古程序员”,写程序操纵计算机是很有乐趣的。 4.比一个Pragmatic Programmer更能折腾的是两个Pragmatic Programmer...
There is an article about how to make a "Squeak 3D Browser".And the nice guy who wrote the article also put a flash demo.It's an amazing demo to show how interesting programming in Squeak could be.But I don't like the too-many-GUI-way which the guy used. I thought there might be a more pragmatic way to do the same thing so I spent an hour on making a script, and finally I got it ;-) Preferences enable: #systemWindowEmbedOK.
myBrowserWindow := Browser new openEditString:#Hello. myBrowserWindow setLabel:'System Browser'; bounds: (Rectangle left:20 right:600 top:20 bottom:400).
my3DIDE := Wonderland new. my3DIDE getDefaultCamera getMorph bounds: (Rectangle left:20 right:800 top: 20 bottom: 600).
my3DBrowser := my3DIDE makePlaneNamed: 'System Browser'. my3DBrowser setY:50; turn:#right turns:0.38; setProperty: #adjustToTexture toValue: true; setTexturePointer: myBrowserWindow; setProperty: #activeTexture toValue: true.
Try it and have fun!
RSS may be one of the most delusive terms, it could stand for 'RDF Site Summary', 'Rich Site Summary', 'Really Simple Syndication' or other things.Today I introduce a brand-new meaning of RSS: Ruby.Smalltalk.Scheme, which are my beloved top 3 programming languages in ascending order.The more I write in Ruby, the more I think in Smalltalk;And the more I think in Smalltalk, the more I love Scheme.Abstract concepts in Scheme, think objects in Smalltalk and write codes in Ruby, cool!
Frank and I collected some Hibernate tips, and we named them with some sexy names ;-) This afternoon I tried to replace hibernate by JSR220 persistent API in a typical web application, and I found some new tips in JSR220 Persisntent API (Frank, I'm here waiting your names).
btw: I'm a believer in Peter Coad's Color Modeling approach, all the following tips are modeled in the approach. I recommend you the greate book 'Java Modeling in Color with UML' if you'd no idea about the Color UML.
1. Annotating Description with @Embeddable, and the Thing it describe with @Entity
Take Product and ProductDescription for example.
1 @Embeddable 2![](/Images/OutliningIndicators/ExpandedBlockStart.gif) class ProductDescription { 3 private String name; 4 private String description; 5 . and getters and setters 6 } 7![](/Images/OutliningIndicators/None.gif) 8 @Entity(access = AccessType.FIELD) 9![](/Images/OutliningIndicators/ExpandedBlockStart.gif) class Product { 10![](/Images/OutliningIndicators/InBlock.gif) 11 private ProdcutDescription description; 12![](/Images/OutliningIndicators/InBlock.gif) 13 } 14![](/Images/OutliningIndicators/None.gif)
In hiberante, we could make the ProductDescription as an embedded persistent object of Product via the component tag.If the ProductDescription should be embedded in another object, we've to declare it once more. Things become more convenient in JSR 220, because non-transite fields, whose class is annoated with @Embeddable, are treated as embedded persistent objects automatically, we'd have fine-grained persistent objects without long-winded config file.
2.Avoiding Database Field in Domain Model Using @AttributeOverride
It's claimed that Annotaton could simplify development, but considering the cost of hard-coding mapping information in source codes I prefer some other complicated ways.It's too ugly to write code like following.
1 @Entity(access = AccessType.FIELD) 2 @Table(name="TAB_PRODUCT") 3![](/Images/OutliningIndicators/ExpandedBlockStart.gif) class Product { 4![](/Images/OutliningIndicators/InBlock.gif) 5 @Basic 6 @Column(name="NAME", nullable=false, length=512) 7 private String name; 8 }
It's sucks but could be avoided.
1 @EmbeddableSuperclass 2![](/Images/OutliningIndicators/ExpandedBlockStart.gif) class Product { 3![](/Images/OutliningIndicators/InBlock.gif) 4 @Basic 5 private String name; 6 } 7![](/Images/OutliningIndicators/None.gif) 8 @Entity 9 @AttributeOverride(name="name", column=@Column(name="Name")) 10 @Table(name="TAB_PRODUCT") 11![](/Images/OutliningIndicators/ExpandedBlockStart.gif) class PersistentProduct extends Product { 12 }
We could separate the domain model and the persistent model by inheritance.Although we could do the same thing in hiberante too, we have to provide lots of configuration files.Once more we'd have hierachical persistent objects without long-winded config file.
3.Avoiding Database Primary Key in Domain Model
In most O/R mapping framework, we are always asked to give a primary key or identifier to our domain model, and we are suggested using meanless identifier instead of domain-specified identifer.It's kinda of Domain Pollution.Although we couldmake the domain model more clearer via inheritance, it's pointless for common usages. Fortunately, we HAVE TO separate domain model and persistent model(as above mentioned), so that we can throw this bad habit away conviniently :D
1 @EmbeddableSuperclass 2![](/Images/OutliningIndicators/ExpandedBlockStart.gif) class Product { 3 } 4![](/Images/OutliningIndicators/None.gif) 5 @Entity 6![](/Images/OutliningIndicators/ExpandedBlockStart.gif) class PersistentProduct extends Product { 7![](/Images/OutliningIndicators/InBlock.gif) 8 @Id private long primaryKey; 9 }
Conclusion
After two hours walking around JSR220 persistent API, I figured out that JSR220 mixed ugly into convenience... and it's quite easy to write ugly codes in jsr220...so that we've to insist on using OO and the convenience jsr220 provided to make our domain model as clear as possible. Although JSR220 is more ugly than hibernate, we could get clearer domain model in jsr220 rather than hibernate, because we could not stand the unly in jsr220...weird way to make us keeping more OO...cool isn't it...
Tao Wen , a friend of mine and a new ThoughtWorker, put a comment on my last article.
so, you think the consistent thing means a lot? But how do you think the gracious or elegant feeling mean to the real expressiveness of business requirements? We can see lisp is very successful in researching area because of the simplicity nature inside which is also true to Smalltalk. But why they aren't successful in biz world? I think that is the problem you should further study, "how can the consistent feeling mean something to our everyday programming life?".
First of all, I should admit that I'd never thought about this question before, becasuse I think hankering after consistent concepts is some nature of us. The question enlightens me to thinking deep. Here are my points.
1.Complexity
Inconsistent concepts bring complexity.I'll give two examples.
First one is something in Lisp. Lisp has a very simple computing model called λ Calculation. There are three elements in this computing model: Function, λ Operator and · Operator. λ Operator picks up free variable from the function, and · Operator applies the variable with a concrete value. The whole evaluation-application thing is what we called symbolic algebra in math.For example, if we've a function whose expression is x + y.
((λx (λy . x + y)·3)·4)
=((λx. x+3)·4)
=4 + 3
=7
Since Lisp is kinda implementation of λ Calculation in computer, we could do the same thing in Lisp (and its dialects).
(define f (lambda x (lambda y (+ x y))))
(apply (apply f 3) 4)
or we could use some syntax sugar
(define (f x y) (+ x y))
((f 3) 4)
Most of things in Lisp are focus on evaluation, and the evaluation should be done in a simple consistent way: evaluating, applying value and then returning the value without any change to enviroment. It is the very same way that we do something in math. But variable assignment was introduced to Lisp, so some functions which change enviroment instead of simple evaluating came to Lisp.For example:
(define (bad-f x y)
(begin
(set! z 5)
(+ x y)))
That bad function changes value of symbol 'z' while evaluating x + y. It is the variable assignment which breaks the consistent concepts. The consistent concepts suggests that function should do and only do evaluation. We must find a more complicated computing model to replace the simple elegant mathematical one.Here inconsistent brings complexities in computing model.
The other example comes from Java. Java is considered to be an object-oriented programming language with single root type system. Conceptually speaking, everything should be an object.But for some perfermance reason, Java has some primitive types (such as int, double, float etc.) too.Those primitive types introduced a algebraic world which paralleled to the object world, and broke the consistent concepts.Althought Java has some wrapper classes for those primtive types, they are not enough for filling the gulf between object and algebra. We could use new Integer(3) to represent 3 in object world, but we could not simply translate
3 + 4 * 5
to
new Integer(3) + new Integer(4) * new Integer(5)
Algebraic calculation and object message sending are totally different. Although we could do that in Java 5, it also has lots of differences.Auto-boxing treats object as primitive instead of operator overloading in C++ which treats operator as object message sending.So in my opinion, auto-boxing in java5 is awful. Assginments and storage of primitive type and object have lots inconsistent semantics too. This inconsistent would bring complexity when we use Collection API(Remember Primitive Collection in Commons Collection Project? )
2. Side-Effect
The mainly cause of side-effect is putting some inconsistent concepts together. I'll also give two examples.
First example is the well-known side-effect in functional programming, which is caused by mixing Von-Nouma-style-things into λ Calculation. There are lots of discussion about this topic in functional programming community.It's meanless to repeat that again.
Second example comes from half-blooded object-oriented languages, for we'll find both object-oriented things and procedure-oriented things in these languages, and it'll be much more easier to write code in procedural way instead of object-oriented way.For example, we'd write code like this:
1 if (order.getState() == Order.CANCEL) 2 do something for cancel; 3 else if(order.getState() == Order.PAID) 4 do something for paid; 5 else if(order.getState() == Order.DELIVERY) 6 do something for delivery instead of :
1![](/Images/OutliningIndicators/ExpandedBlockStart.gif) public abstract OrderState { 2![](/Images/OutliningIndicators/InBlock.gif) 3 protected Order _order; 4 5![](/Images/OutliningIndicators/ExpandedSubBlockStart.gif) protected OrderState(Order order) { 6 _order = order; 7 } 8 9 public abstract void handler(); 10 } 11![](/Images/OutliningIndicators/None.gif) 12![](/Images/OutliningIndicators/ExpandedBlockStart.gif) class CancelState extends OrderState { 13![](/Images/OutliningIndicators/InBlock.gif) 14![](/Images/OutliningIndicators/ExpandedSubBlockStart.gif) public void handler() { 15 do something for cancel; 16 } 17 } 18![](/Images/OutliningIndicators/None.gif) 19 order.getState().handle(); For the keywords, it's convenient to write procedural codes as first segment, but for easy maintenance, it's better to write object-oriented codes as the second one.There are two inconsistent way to write code, and the side-effect is depend on how object-oriented the language is. It's more easy to write code like first one in C++ , and more easy to write code like second one in Ruby and Smalltalk. Actually, it's very hard for me to write code like first one in Smalltalk. 3. Constructing Software in a Recursive Way There is a more large topic, maybe I'd better talk about this latter. 4. Elegance Some aesthetic feelings comes from consistent concepts.I'll have the feeling of elegance if I could understand some complicated things via some consistent concepts.Take Newtonian System and Maxwell formula for example, they both are the elegant way to resolve problem via few consistent concepts.
|
|
随笔:36
文章:0
评论:93
引用:0
| 日 | 一 | 二 | 三 | 四 | 五 | 六 |
---|
27 | 28 | 29 | 30 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
|
常用链接
留言簿(21)
随笔分类
随笔档案
搜索
最新评论
![](/images/xml.gif)
阅读排行榜
评论排行榜
|
|