领导力大挑战
在实施Scrum项目的过程中,Scrum Master的角色是相当关键的,因为他是团队的推动者。本文围绕什么是仆人式领导、仆人式领导的起源、如何将领导力传达给团队、Scrum Master作为仆人式领导者的角色展开叙述,同时重点阐述仆人式领导者应有的基本内外特征。
该内容已经被标记书签!
标记书签错误,请重试!

作者 郑晔 发布于 2011年9月10日
自动化脚本之于软件开发,犹如地基之于建筑。
在软件开发过程中,缺乏一个好的自动化脚本,与之相伴的往往是日常的开发工作举步维艰:
在本文中, 我们将以一个Java的web项目为例,展示一个好的“地基”应具备的一些基本素质。在这里,用做自动化的工具是buildr。
buildr是一种构建工具,它专为基于Java的应用而设计,也包括了对Scala、Groovy等JVM语言的支持。相比于ant和maven这些Java世界的“老人”,buildr算是小字辈,也正是因为年轻,它有着“老人”们不具备的优势:
简而言之,它满足了我们选择工具的基本原则:“易者易为,难者可为”。
请注意:下面所有的内容并不只是buildr的独家专利,而是每个构建工程都应该具备的,差异只在于,选择不同的工具,实现的难度略有差异而已。
让我们从一个简单的buildfile——buildr的脚本——起步:
GUAVA = 'com.google.guava:guava:jar:r09'
define 'killer' do
project.version = '0.0.1'
define 'domain' do
compile.with GUAVA
package :jar
end
define 'web' do
DOMAIN = project('killer:domain').packages
compile.with DOMAIN
package(:war).with(:libs=>DOMAIN)
end
end
我们先来看看从这个简单的buildfile中,我们可以得到什么。
这个项目里有两个子项目:domain和web。从架构的角度来看,一个项目从一开始就划分出这样的模块是有好处的:
使用buildr划分模块是非常简单的,只要在buildfile里声明模块,项目的根目录下同名的子目录就是对应的模块。
虽然在buildfile没有直接体现出来,但这里有个缺省的文件布局。一个统一的规则省去了我们从头规划的苦恼。遵循缺省的布局规则,buildr自己就会找到相应的文件,进行处理。
这个布局规则实际上就是Maven的布局规则,如图所示,两个子项目都拥有自己的目录,其结构基本一致:
此外,这里还有稍后会提及的:
这就是所谓的Convention over Configuration。当然,buildr是支持自定义文件布局的,详情请参见文档。
有了这个基本的buildfile,我们就可以开展日常的工作了。buildr自身支持很多命令,比如:
想要了解更多的命令,可以运行下面的命令:
buildr -T
在这个不测试都不好意思自称程序员的年代,测试,尤其实现级别的测试,诸如单元测试、集成测试,已经成了程序员的常规武器。
诚如上面所见,src/test/java就是我们的测试文件存放的目录。对于Java项目,JUnit是缺省的配置,只要在这个目录下的Java类继承自junit.framework.TestCase(JUnit 3),或是,在类上标记了org.junit.runner.RunWith,抑或在方法上标记了org.junit.Test(JUnit 4)。buildr就会找到它们,并帮我们料理好编译运行等事宜。约定的力量让我们无需操心这一切。
依赖管理一直是一项令人头疼的问题,也是让许多开发人员搭建纠结于开发环境搭建的一个重要因素。
各种语言的社区分别给出了自己的依赖管理解决方案,对于Java社区而言,一种比较成熟的解决方案来自于Maven。它按照一定规则建立起一个庞大的中央仓库,成熟的Java库都会在其中有一席之地。
于是,很多新兴的构建工具都会建立在这个仓库的基础之上,buildr也不例外。在前面的例子里面,domain依赖了Guava这个库。当我们开始构建应用时,buildr会自动从中央仓库下载我们缺失的依赖。
不仅仅是依赖,我们还可以拿到对应的文档和源码:
如果不知道如何在buildfile里编写依赖,那mvnrepository.com是个不错的去处,那里针对不同的构建工具都给出了相应的依赖写法。
除非这个工程是用IDE创建出来的,否则把工程集成到一个IDE里通常要花费很大的力气。所幸,buildr替我们把这些工作做好了。我们只要键入一个命令即可,比如与IntelliJ IDEA集成,运行下面的命令:
buildr idea
它会生成一个IDEA的工程文件,我们要做的只是用IDEA打开它。同样的,还有一个为Eclipse准备的命令:
buildr eclipse
不知道你是否有这样的经验,初到一个项目组,开始为一个项目贡献代码之前,先需要花几天时间,在不同的人的协助之下把环境搭出来,为的只是在自己的机器上能够把应用构建出来。
而现在,有了这样的自动化脚本,一个项目组新人的行为模式就变成了:
迄今为止,我们看到的只是一个基本的buildfile,这些命令也是buildr内置的一些基本能力,也就是所谓的“易者易为”。
接下来,我们将超越基础,做一些“难者可为”的东西。
在实际的开发中,我们经常会遇到不同的环境,比如,在开发环境下,数据库和应用服务器是在同一台机器上,而在生产环境下,二者会部署到机器上。这里所列举的配置功能,只是最简单的例子,而实际情况下,不同的环境下,会有各种差异,甚至需要执行不同的代码。
一种解决方案是为数不少的“直觉式”设计采用的方案,在代码里根据条件进行判断,可想而知,无处不在的if..else很快就会把代码变成一团浆糊,更糟糕的是,这些信息散落在各处。
另一种方案是在自动化脚本中支持,buildr让这个工作变得很简单。
配置信息
使用buildr,配置信息可以放到一个名为profile.yaml的文件里,下面是一个例子:
development:
db:
url: jdbc:mysql://localhost/killer_development
driver: com.mysql.jdbc.Driver
username: root
password:
jar: mysql:mysql-connector-java:jar:5.1.14
production:
db:
url: jdbc:mysql://deployment.env/killer_production
driver: com.mysql.jdbc.Driver
username: root
password: ki1152
jar: mysql:mysql-connector-java:jar:5.1.14
我们看到,针对不同的环境,有不同的数据库配置,在buildfile里可以这样引用这些配置:
db_settings = Buildr.settings.profile['db']
随后,我们就可以使用这个配置,比如生成一个配置文件:
task :config do
CONFIG_PROPERTIES = <<EOF
jdbc.driverClassName= #{db_settings['driver']}
jdbc.url=#{db_settings['url']}
jdbc.username=#{db_settings['username']}
jdbc.password=#{db_settings['password']}
EOF
File.open('config.properties'), "w") do |f|
f.write config
end
end
有了这样的基础,只要我们指定不同的环境就会产生不同的配置。
系统组件
在buildr里,有一个叫做ENV['BUILDR_ENV']的变量,这是buildr内置的一个变量,通过它,我们可以获得当前环境的名字,在这个例子里,它可以是development或是production。
有了这个变量,我们可以进行更加深度的配置,比如,在测试环境下,我们可以采用一些假的实现,让整个系统运行的更快。
下面是一个例子,有一个搜索组件的配置文件,在生产环境下,它会采用真实的搜索引擎实现,而在开发环境时,它只是一个简单内存实现。我们把不同环境的实现放到不同的配置文件里。
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="searcher" class="com.killer.SuperSearcher"/>
</beans>
(searcher.production.xml)
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="searcher" class="com.killer.InMemorySearcher"/>
</beans>
(searcher.development.xml)
要使用这个组件只要引用searcher.xml,把searcher拿过来用就好了:
<import resource="searcher-context.xml"/>
...
<property name="searcher" ref="searcher"/>
接下来,用一个task就可以处理这些差异:
task :searcher do
cp "searcher.#{ENV['BUILDR_ENV']}.xml", "searcher.xml"
end
其实,实现这一点真正困难的并不在于配置文件,而在于这些组件的设计。只有识别出这些组件,把它们独立出来,才会可能根据不同的环境进行配置。没有恰当的抽象,自动化脚本也无能为力。
有了上面准备的基础,我们可以通“-e”这个选项,在命令行中指定我们的环境。下面的命令会让我们得到production的配置,不论是配置信息,还是系统组件。:
buildr -e production
在团队开发中,统一的代码风格很重要。即便我们很认真,疏忽也在所难免。Java世界里,checkstyle常常扮演了代码检察官角色。
坏消息是,buildr并不提供对checkstyle的直接支持。好消息是,buildr可以集成ant task,所以,我们通过ant task集成checkstyle。
CHECKSTYLE = transitive('checkstyle:checkstyle:jar:4.4')
task :checkstyle do
begin
ant('checkstyle') do |ant|
rm_rf 'reports/checkstyle_report.xml'
mkdir_p 'reports'
ant.taskdef :resource=>"checkstyletask.properties",
:classpath=>Buildr.artifacts(CHECKSTYLE)
.each(&:invoke).map(&:name).join(File::PATH_SEPARATOR)
ant.checkstyle :config=>"tasks/checkstyle_checks.xml",
:maxWarnings=>0 do
ant.formatter :type=>'plain'
ant.formatter :type=>'xml',
:toFile=>"reports/checkstyle_report.xml"
ant.property :key=>'javadoc.method.scope', :value=>'public'
ant.property :key=>'javadoc.type.scope', :value=>'package'
ant.property :key=>'javadoc.var.scope', :value=>'package'
ant.property :key=>'javadoc.lazy', :value=>'false'
ant.property :key=>'checkstyle.cache.file',
:value=>'target/checkstyle.cache.src'
ant.property :key=>'checkstyle.header.file',
:value=>'buildconf/LICENSE.txt'
ant.fileset :dir=>"domain/src/main/java",
:includes=>'**/*.java',
ant.fileset :dir=>"web/src/main/java",
:includes=>'**/*.java'
end
ant.xslt :in=>'reports/checkstyle_report.xml',
:out=>'reports/checkstyle_report.html',
:style=>'tasks/checkstyle-noframes.xsl'
end
end
end
这里的绝大多数内容checkstyle的ant task介绍都可以很容易了解到,唯一需要注意的点在于checkstyle任务的maxWarnings属性。
maxWarning表示可以容忍的最大警告数,也就是说,当checkstyle检查的警告数大于这个数字时,构建就会失败。在这里,我们将其设置为0,换句话说,我们不接受任何警告。
在实践中,对低级错误容忍度越低的团队,代码质量往往也会越高。
像这种独立性很强的任务,我们通常会把放到一个独立的文件中。在《文件布局》一节,我们曾提及tasks目录,其中存放的就是自定义任务,buildr在启动时会自动加载该目录下的这些任务。
如果熟悉rake,你会发现上面的task就是一个标准的rake task。实际上,buildr是集了多个构建工具的本领于一身的。把上面的代码放到checkstyle.rake,然后放到tasks目录下。运行下面的命令,就可以对代码执行静态检查:
buildr checkstyle
测试覆盖率这东西,高了不代表有多好,但低了肯定是有问题。
当我们把测试覆盖率限制为100%,其结果是全部内容都会有测试覆盖。坚持这样的标准,在做重构的时候,可以比较放心。
工具支持
buildr缺省支持一个测试覆盖率工具——cobertura,所以,一切做起来很简单。
require 'buildr/java/cobertura' cobertura.check.branch_rate = 100 cobertura.check.line_rate = 100
(buildfile)
在命令行里运行如下命令就可以运行测试,进行检查,生成报告:
buildr cobertura:html cobertura:check
如果覆盖率不达标,构建就会失败。这时,我们可以通过它生成的报告来看,到底是哪里没有覆盖。报告位于
reports/cobertura/html/index.html
例外情况
真的是所有代码都会在这个100%的监控之下吗?不一定。有一些代码只是为了调用一个特定的API,这样的代码是否100%意义不大,这样的测试本质上是在API写测试,而非自己的代码逻辑。所以,这种代码会被排除在外。
cobertura.exclude /.*.integration.*/
但是,这样的代码同样会有相应的集成测试,只是不在单元测试的层面。一个基本的原则是,被排除的代码要尽可能少。
作为一个专业程序员,我们应该保证自己提交的代码不会对代码库造成破坏。但怎么才算是不破坏呢?也许我们要编译、运行单元测试、打包,也许我们还要进行静态检查,查看测试覆盖率,也许还要进行更多的检查,运行各种各样的测试。有了前面的基础,我们可以做一个task,把所有这些都依赖上去。
好,有了这个task,要提交代码,我们会怎么做呢?
显然,这是一个说多不多,说少也不少的操作,对于这种繁琐的操作,如果能够自动化,自然是最好的选择。下面就是一段这样的脚本,这里用的到版本控制系统是git。
首先看到的是版本控制系统部分:
namespace :git do
def sys(cmd)
puts cmd
raise "System execution failed!" unless system(cmd)
end
def get_info(name, prompt)
begin
value = File.open(".#{name}") { |f| f.read }.strip
rescue
value = ""
end
prompt += " (#{value})" unless value.empty?
new_value = Readline::readline("[#{prompt}]: ").strip
value = new_value unless new_value.empty?
File.open(".#{name}", "w") { |f| f.write(value) }
value
end
def commit
dev_name = get_info('pair', 'Pair')
story_number = get_info('story', 'Story #')
comment = get_info('comment', 'Comment')
commit_cmd = %Q(git commit -am "#{dev_name} - KILLER-#{story_number} - #{comment}")
sys(commit_cmd)
end
def add(files)
files.each { |file| sys("git add #{file}") }
end
def add_files(files)
puts "Add the following new files:\n#\t#{files.join("\n#\t")}\n"
reply = Readline::readline("[Y/N]")
return add(files) if reply.strip.downcase.start_with?('y')
raise 'new files should be added before commit'
end
task :add do
files = `git status -s | awk '/\\?\\?/ {print $2}'`
files = files.split("\n")
add_files(files) if files.size > 0
end
def nothing_to_commit?
`git status -s`.empty?
end
task :pull do
sys('git pull')
end
task :push do
sys('git push')
end
task :status do
sys('git status')
end
task :commit => :add do
commit unless nothing_to_commit?
end
end
这里,我们看到了git的一些基础操作,比如pull、push、commit等等,并且,我们在基础操作之上进行了封装,让使用更便捷。比如:
之所以在这里还要填写名字,而不仅仅是利用git自有的用户名,因为在开发的过程中,我们可能是结对开发,显然一个用户名是不够的。
有了这个基础,我们继续向前,创建一个task,把提交之前要做的事情都放在这里:
task :commit_build => [:clean, :artifacts, :checkstyle,
"cobertura:html", "killer:domain:cobertura:check",
"killer:web:cobertura:check"]
以上面这个task为例,实际上包括编译、测试、静态检查和测试覆盖率检查,我们可以根据自己的需要加入更多的东西。
接下来,把版本控制部分结合进去,就是我们的提交脚本了。
task :commit => ["git:commit", "git:pull",
:commit_build, "git:push", "git:status"]
有了这个基础,我们就可以提交代码了,而不用担心忘记了什么:
buildr commit
在执行过程中,任何一步失败都会让整个提交过程停下来,比如,当pull代码产生冲突,或是运行测试之后,push代码时,我们发现又有人提交,提交过程就会停止,错误的代码是不会提交的。
数据库脚本通常不像代码那样受人重视,但在实际的发布过程中,它常常把我们弄得焦头烂额。
dbdeploy为我们提供了一种管理数据迁移的方式,buildr没有提供对dbdeploy直接的支持,但如我们之前所见,ant task可以帮我们打造一条直通之路。
namespace :db do
DB_SETTINGS = Buildr.settings.profile['db']
def dbdeploy(options)
ant('dbdeploy') do |ant|
ant.taskdef(:name => "dbdeploy",
:classname => "com.dbdeploy.AntTarget",
:classpath=>DBDEPLOY_CLASSPATH)
ant.dbdeploy(options)
end
end
task(:migrate) do |t, args|
dbdeploy(:driver => DB_SETTINGS['driver'],
:url => DB_SETTINGS['url'],
:userid => DB_SETTINGS['username'],
:password => DB_SETTINGS['password'],
:dir => "#{db_script_dir}/migration")
end
end
这里,我们用到之前提及的管理配置的方式,配置信息存放在profile.yaml里。另外,数据库迁移文件存放在数据库脚本目录(db_script_dir)的migration子目录下。
有了这段task,我们就可以进行数据库迁移了:
buildr db:migrate
对于一个Web应用而言,仅仅测试打包还是不够的,我们还要把它部署到Web服务器上。这是一个基础任务,有了它,我们就可以使用一些web测试框架,对我们的系统进行验收测试。
通常在开发过程中,我们会选择一个部署起来很容易的Web服务器,在Java的世界里,jetty往往扮演着这样的角色。当然,这样做的前提是,我们编写的是一个标准的Java WebApp,没有用到特定于某个具体应用服务器的API,换句话说,我们的Web应用是跨应用服务器的,事实上,这种做法是值得鼓励的。
使用特定于具体应用服务器的API,其结果只能是与这个服务器产生耦合,而通常提供这个API的服务器对于开发并不那么友好,在其上部署的周期会很长,这无疑会大大降低开发效率。
一个好消息是,buildr有很好的对jetty的集成。
require 'buildr/jetty'
define 'killer' do
define 'web' do
…
task("jetty-deploy"=>[package(:war), jetty.use]) do |task|
jetty.deploy("http://localhost:8080", task.prerequisites.first)
end
task("jetty"=> ["jetty-deploy"]) do |task|
Readline::readline('[Type ENTER to stop Jetty]')
end
end
end
这里,我们启动了一个jetty,启动之前,我们需要确保已经有了可以部署的WAR,所以这个部署任务要依赖于package(:war)。最后的jetty task给了我们一个手工停止Jetty的机会。我们可以这样将启动它:
buildr killer:web:jetty
写好代码,完成部署,还少不了验收测试。
对于一个Web项目,我们可能会考虑采用selenium或是waitr之类的自动化测试框架进行测试。不过,通常直接用这些框架去写通常会把业务需求和测试实现细节混合起来,结果往往不如我们预期。
Cucumber的出现给了我们一种分离业务需求和实现细节的方式。对我们这个项目而言,不好的消息是,想到Cucumber,出现在我们脑子里的是Ruby语言,好消息是buildr就是Ruby语言。下面就是一段脚本,把它放到buildfile里,就可以运行acceptance/features目录下那堆feature文件了。
require 'cucumber'
require 'cucumber/rake/task'
define 'killer' do
define 'web' do
…
Cucumber::Rake::Task.new(:acceptance => "jetty-deploy") do |t|
t.cucumber_opts = ["acceptance/features"]
end
end
end
有了它,我们就可以运行我们的验收测试了:
buildr killer:web:acceptance
至此,我们已经展示了一个基本的自动化脚本。正如我们所见,作为地基的自动化脚本,仅仅有编译、测试、打包这些基本操作是远远不够的。即便是这里所列出的这些,也不过是一些通用的任务,每个项目都应该把项目内的繁琐操作自动化起来,比如,在我参与的实际项目中,我们会把部署到用户验收测试(User Acceptance Testing,简称UAT)环境的过程自动化起来。
不过,除了脚本自身,为了让地基真正的发挥作用,还需要有一些实践与之配合,比如有了持续集成,提交脚本才好发挥最大的威力,比如有了合适的测试策略,开发人员才能在一个合理的时间内完成上的本地构建。
软件开发永远都不是一个单点,只有各方各面联动起来,才会向着健康的方向前进,而所有一切的基础,就是自动化。
感谢张凯峰对本文的审校。
给InfoQ中文站投稿或者参与内容翻译工作,请邮件至editors@cn.infoq.com。也欢迎大家加入到InfoQ中文站用户讨论组中与我们的编辑和其他读者朋友交流。
在实施Scrum项目的过程中,Scrum Master的角色是相当关键的,因为他是团队的推动者。本文围绕什么是仆人式领导、仆人式领导的起源、如何将领导力传达给团队、Scrum Master作为仆人式领导者的角色展开叙述,同时重点阐述仆人式领导者应有的基本内外特征。
在多线程并发编程中Synchronized一直是元老级角色,很多人都会称呼它为重量级锁,但是随着Java SE1.6对Synchronized进行了各种优化之后,有些情况下它并不那么重了,本文详细介绍了Java SE1.6中对于锁的性能优化,以及锁的存储结构及升级过程。
本次分享将首先介绍现代富文本编辑器的组成和实现,然后结合UEditor的开发过程,与参会者分享UEditor在设计和实现的过程中,所涉及到的核心功能的细节实现。
本次演讲视频录制于百度技术沙龙。
我们所开发的应用程序大多都需要提供一个图形用户界面(GUI)。关于GUI应用的架构设计,已经有了Form & Control、MVC,、MVP、 Passive View等多种模式。模式可以帮助我们建立优雅的架构,但前提是弄清楚模式的应用场景。弄清楚GUI应用面临的设计上的问题,有助于我们正确的挑选设计方案。
MongoDB是一种非常易用的NoSQL方案,Brian C. Dilley在这篇文章里介绍了MongoDB的优劣势,并介绍了MJORM项目。MJORM用于MongoDB,是一个没有注解的Java ORM库。
随着网络基础设施的逐步成熟,从RPC进化到Web Service,并在业界开始普遍推行SOA,再到后来的RESTful平台以及云计算中的PaaS与SaaS概念的推广,分布式架构在企业应用中开始呈现出不同的风貌,然而殊途同归,这些分布式架构的目标仍然是希望回到建造巴别塔的时代,系统之间的交流不再为不同语言与平台的隔阂而产生障碍。
5 条回复
关注此讨论 回复