乐者为王

Do one thing, and do it well.

对《什么样的Java代码看上去比较专业?》的评论

原文:http://blog.csdn.net/michaellufhl/article/details/5996261

本来是要回复在该博文下面的,结果也不知道CSDN是不是在抽疯,死活提交不上,一直显示“正在提交,请稍后……”,所以就只好贴在这里了。

“用最新的语言特性?”很搞笑,恰恰说明你不够专业,因为这表明你每天都在追逐所谓新的东西,而不是在做真真正正的事。

“充足的代码注释”也不完全正确,关键的注释才是好的,而不必是充足的。你在阅读人家代码的时候有多少时间是在看注释呢,所以说注释一定要写在关键点上,不过这一点大多数人都很难做到。

好的命名非常重要,而不仅仅是命名规范,一个好的命名可以代替大段的注释,一看命名就知道这是干什么用的,这时还需要写注释吗?

再次说一句,CSDN真的真的很垃圾!

Rails中做计划任务

在平时的开发中,经常会遇到一些计划任务的需求。比较了几个不同的插件,觉得rufus-scheduler不错,用法也很简单,支持某个时间点做某事,某个时间段做某事等。

安装插件:

1
gem install rufus-scheduler --source http://gemcutter.org

调用也很简单,在initializers文件夹下新建任务文件task.rb:

1
2
3
4
5
6
7
8
require 'rubygems'
require 'rufus/scheduler'

scheduler = Rufus::Scheduler.start_new
scheduler.cron '0 22 * * 0-6' do
  # every day of the week at 22:00 (10pm)
  puts Time.now
end

Cron格式:

  • 第1列分钟0-59
  • 第2列小时0-23(0表示子夜)
  • 第3列日1-31
  • 第4列月1-12
  • 第5列星期0-6(0表示星期天)
  • 第6列要运行的命令

编写无需注释的代码

英文原文:https://blog.codinghorror.com/coding-without-comments/

如果代码中充满大量注释是好的话,那么在代码中有无数的注释必定是更好的,真的是这样吗?那可未必。过度是好注释变坏的一种方法:

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
'*************************************************
' Name: CopyString
'
' Purpose: This routine copies a string from the source
' string (source) to the target string (target).
'
' Algorithm: It gets the length of "source" and then copies each
' character, one at a time, into "target". It uses
' the loop index as an array index into both "source"
' and "target" and increments the loop/array index
' after each character is copied.
'
' Inputs: input The string to be copied
'
' Outputs: output The string to receive the copy of "input"
'
' Interface Assumptions: None
'
' Modification History: None
'
' Author: Dwight K. Coder
' Date Created: 10/1/04
' Phone: (555) 222-2255
' SSN: 111-22-3333
' Eye Color: Green
' Maiden Name: None
' Blood Type: AB-
' Mother's Maiden Name: None
' Favorite Car: Pontiac Aztek
' Personalized License Plate: "Tek-ie"
'*************************************************

我经常遇到注释来自那些开发者,他们似乎没有意识到代码已经告诉我们它是如何工作的;我们需要注释告诉我们它为什么这样工作。代码注释被如此广泛地误解和滥用,你可能会发现自己根本不知道它们是否值得使用。注意你需要的东西。下面是没有任何注释的代码:

1
2
3
4
5
r = n / 2;
while ( abs( r - (n/r) ) > t ) {
  r = 0.5 * ( r + (n/r) );
}
System.out.println( "r = " + r );

知道这段代码做什么吗?它是完全可读的,但它到底做什么?

让我们添加一段注释。

1
2
3
4
5
6
// square root of n with Newton-Raphson approximation
r = n / 2;
while ( abs( r - (n/r) ) > t ) {
  r = 0.5 * ( r + (n/r) );
}
System.out.println( "r = " + r );

这应该就是我想要的,真的是这样吗?这是在完全没有注释和每隔一行代码就有精心格式化的史诗般的注释两个极端之间某种满意的、中间道路的妥协?

不全是。与其添加一段注释,我会重构成这样:

1
2
3
4
5
6
7
8
private double SquareRootApproximation(n) {
  r = n / 2;
  while ( abs( r - (n/r) ) > t ) {
    r = 0.5 * ( r + (n/r) );
  }
  return r;
}
System.out.println( "r = " + SquareRootApproximation(r) );

我没有添加任何注释,然而这段神秘的代码现在是完全可以理解的。

尽管注释本身没有好坏,它们却常常被用作代码的支撑。你应该总是写你的代码就跟注释不存在一样。这会迫使你以你能在人力所及的范围想出最简单、最直白、最自文档化的方式写你的代码。

当你重写、重构和重新架构你的代码多次以使你的开发者同事容易阅读和理解时——当你想象不到任何可能的方式你的代码可以被修改而变得更直接和明显时——那时,也只有在那时,你才不得不添加注释解释你的代码做什么。

正如Steve指出的,这是初级和高级开发者之间的一个关键区别:

在过去,一下子看太多的代码老实说超过了我的接受能力,当我不得不处理它时我通常会试图重写它或者至少添加大量的注释。然而今天,我只是艰难地通过它没有抱怨(太多)。当我脑中有个明确的目标和一段复杂的代码要去写的时候,我花费时间去实现它而不是给自己讲述关于它的故事[在注释里]。

初级开发者依赖注释去讲述故事,当他们应该依赖代码去讲述故事的时候。代码是叙述旁白,它们有其自身的价值,但并不意味着替换(故事里的)情节、人物刻画和背景。

或许这就代码注释不可告人的小秘密:写出好的注释你必须是个好作者。注释不是用于编译器的代码,它们是用来和其他人交流想法的言语。虽然我确实要赞美我的程序员同事,但我不能说和其他人有效沟通正是我们的强项。我看过来自我团队中开发者的三段电子邮件,几乎融化了我的大脑。这些是我们信任的人在我们的代码中写的清晰的、可以理解的注释?我以为我们中的一些人可能会更好地坚持我们的优势——也就是说,为编译器写代码,以尽可能清晰的方式,而把注释仅作为最后采取的手段。

写出好的、有意义的注释是困难的。它和写代码本身一样是门艺术,甚至可能更艺术些。正如Sammy Larbi在注释代码的常见借口里说的,如果你觉得你的代码太过复杂以致没有注释就不能理解,那么你的代码可能只是坏的。重写它直到它不再需要注释。如果经过这些努力后,你仍然觉得注释是必要的,那么务必添加注释。越谨慎越好。

用Rails 2.3打造todolist应用

首先生成项目骨架:

1
2
3
4
rails todolist
cd todolist
script/generate scaffold todo title:string description:text done:boolean due_date:datetime
rake db:migrate

user-todo-association

安装认证和授权插件:

1
2
3
4
5
script/plugin install git://github.com/technoweenie/restful-authentication.git restful_authentication
script/generate authenticated user sessions

script/plugin install git://github.com/greenisus/forgot_password.git
script/generate forgot_password password user

然后将include AuthenticatedSystem移到ApplicationController中:

1
2
class ApplicationController < ActionController::Base
  include AuthenticatedSystem

添加Todo和User的关联:

1
2
3
4
5
class Todo < ActiveRecord::Base
  belongs_to :user

class User < ActiveRecord::Base
  has_many :todos

修改TodosController,将Todo和User绑定:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class TodosController < ApplicationController
  before_filter :login_required

  def index
    @todos = current_user.todos

  def show
    @todo = current_user.todos.find(params[:id])

  def new
    @todo = Todo.new

  def edit
    @todo = current_user.todos.find(params[:id])

  def create
    @todo = Todo.new(params[:todo])
    @todo.user = current_user

  def update
    @todo = current_user.todos.find(params[:id])

  def destroy
    @todo = current_user.todos.find(params[:id])

创建一个应用的首页:

1
script/generate controller home index

为了可以访问到应用首页,需要删除public/index.html文件,并且在routes.rb中添加:

1
map.root :controller => 'home'

将app/views/layouts下的todos.html.erb改名为application.html.erb,然后添加下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
<% if logged_in? -%>
  <div id="user-bar-greeting">
    Logged in as <%= link_to_current_user :content_method => :login %>
  </div>
  <div id="user-bar-action">
    (<%= link_to "Log out", logout_path, { :title => "Log out" } %>)
  </div>
<% else -%>
  <div id="user-bar-action">
    <%= link_to "Log in", login_path, { :title => "Log in" } %> /
     <%= link_to "Sign up", signup_path, { :title => "Create an account" } %>
  </div>
<% end -%>

Todo模型的description字段是text类型,在页面上用textarea表示简单了一点,将它改成使用TinyMCE编辑器。可以使用tinymce_hammer插件来集成。

1
script/plugin install git://github.com/trevorrowe/tinymce_hammer.git

执行以下命令后,将会安装TinyMCE到public/javascript/tiny_mce目录:

1
script/generate tinymce_installation

在layout下的模板中添加下面这条语句:

1
<%= init_tinymce_hammer_if_required %>

将app/views/todos下new.html.erb和edit.html.erb中的

1
<%= f.text_area :description %>

修改为

1
<%= f.tinymce :description, :rows => 10, :cols => 40 %>

去除app/views/todos/index.html.erb中description字段的h方法:

1
<td><%= todo.description %></td><br />

还有就是删除某条Todo后记录就彻底地没了,不能恢复,需要给它加上一剂后悔药,使之可以重新被捞出,acts_as_paranoid插件可以做到这点。

1
2
3
script/plugin install git://github.com/technoweenie/acts_as_paranoid.git
script/generate migration add_deleted_at_to_todos deleted_at:datetime
rake db:migrate

添加acts_as_paranoid到模型中:

1
2
class Todo < ActiveRecord::Base
  acts_as_paranoid

现在,调用这个模型的destroy方法将不会真正地删除记录,只会将记录从视图上移除,在deleted_at里记录删除的时间。当然,你可以在find中使用with_deleted或only_deleted参数得到被隐藏的记录。在Rails 3中在find中使用参数会报ArgumentError,显示如下错误:

1
Unknown key: only_deleted

解决办法是使用以下格式的代码:

1
current_user.todos.only_deleted.find(:all)

至此,一个简单的todolist就算完成了。

代码下载:https://github.com/dohkoos/todolist

启用Windows Server 2008无线网络支持

Windows Server 2008系统安装后,大家会发现虽然无线网卡能够被识别并正确安装了驱动,但是却无法接入到无线网络中。Windows 2008本是为服务器设计的系统,自然不会主动把无线服务安装,所以需要通过服务器管理器来添加无线局域网服务。

运行servermanager.msc进入服务器管理器,选中视图左窗中的“Features”,之后单击视图右窗中的“Add Features”添加功能。进入功能选择界面后,勾选“Wireless LAN Service”,根据向导提示进行安装。如下图:

wireless-lan-service

Plugin not found

使用Ruby 1.8.7和Rails 2.3.5,每次执行script/plugin install命令都出现:

1
Plugin not found: [...]

无论使用何种协议,或是在末尾添加斜杠都不起作用。

出现这个问题的原因是因为Ruby 1.8.7是用mingw32编译的,可以通过ruby -v查看:

1
ruby 1.8.7 (2010-08-16 patchlevel 302) [i386-mingw32]

RUBY_PLATFORM的值是i386-mingw32-{version},而许多库在判断当前操作系统时是这么做的:

1
file.open(RUBY_PLATFORM.match(/mswin/) ? 'NUL', '/dev/null')

这些库只判断了RUBY_PLATFORM里是否含有mswin,如果没有就认为是*nix平台,从而使用/dev/null,结果可想而知,文件必然打开失败,于是就会出现上述错误。

解决方式(推荐使用第4种方式):

方法1:打开script/plugin文件加入一行RUBY_PLATFORM = 'mswin',运行时会打印一条警告说常量重新赋值,不过不影响使用。

方法2:卸载掉1.8.7,重新安装Ruby 1.8.6-p26,这个版本是使用VC6编译的,RUBY_PLATFORM的值是mswin,不会出现平台判断错误。

方法3:使用VC自己编译Ruby 1.8.7,确保RUBY_PLATFORM的值是mswin即可。

方法4:打开RUBY_GEMS/activesupport-2.3.5/lib/active_support/core_ext/kernel/reporting.rb,找到

1
stream.reopen(RUBY_PLATFORM =~ /mswin/ ? 'NUL:' : '/dev/null')

将之修改以下代码即可

1
stream.reopen(RUBY_PLATFORM =~ /mswin|mingw/ ? 'NUL:' : '/dev/null')

使用will_paginate插件给Rails应用添加分页功能

安装will_paginate插件:

1
script/plugin install git://github.com/mislav/will_paginate.git

安裝好插件后,在action中將本來的find方法:

1
@contacts = Contact.all

改为

1
@contacts = Contact.paginate(:page => params[:page])

然后在action对应的view最后加入以下代码:

1
<%= will_paginate @contacts %>

现在便可以使用分页功能了,该语句会产生如下的HTML代码:

1
2
3
4
5
6
<div class="pagination">
  <span class="disabled prev_page">« Previous</span>
  <span class="current">1</span>
  <a href="/contacts?page=2&s=" rel="next">2</a>
  <a href="/contacts?page=2&s=" class="next_page" rel="next">Next »</a>
</div>

下面加入搜索功能,在view的适当位置加入:

1
2
3
4
5
6
<% form_tag contacts_path, :method => 'get' do %>
<p>
  <%= text_field_tag :s, params[:s] %>
  <%= submit_tag "Search", :name => nil %>
</p>
<% end %>

并且将action中的代码修改为:

1
2
3
@contacts = Contact.paginate(
  :page => params[:page],
  :conditions => ["name like ?", "%#{params[:s]}%"])

试着进行搜索,可以看到搜索结果也很好的进行了分页。

此外will_paginate还提供一些分页统计信息:

1
2
3
Total entries: <%= @contacts.total_entries %>
Total pages: <%= @contacts.total_pages %>
Current page: <%= @contacts.current_page %>

最后加上will_paginate推荐的CSS代码:

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
32
33
.pagination {
  padding: 3px;
  margin: 3px;
}

.pagination a {
  padding: 2px 5px 2px 5px;
  margin: 2px;
  border: 1px solid #aaaadd;
  text-decoration: none;
  color: #000099;
}

.pagination a:hover, .pagination a:active {
  border: 1px solid #000099;
  color: #000;
}

.pagination span.current {
  padding: 2px 5px 2px 5px;
  margin: 2px;
  border: 1px solid #000099;
  font-weight: bold;
  background-color: #000099;
  color: #fff;
}

.pagination span.disabled {
  padding: 2px 5px 2px 5px;
  margin: 2px;
  border: 1px solid #eee;
  color: #ddd;
}

在写代码的时候碰到一个问题,网上的文章都使用page_count来表示总的分页数,但我在使用时出现了undefined method 'page_count'错误,把page_count换成total_pages后就好了。

2011/1/4更新

这里使用的是2.3.12版本的will_paginate,暂时还不支持i18n,不过可以通过在app/helps/application_helper.rb中添加以下代码来实现:

1
2
3
4
5
6
7
8
9
include WillPaginate::ViewHelpers

def will_paginate_with_i18n(collection, options = {})
  will_paginate_without_i18n(collection, options.merge(
                        :previous_label => I18n.t(:previous, :default => 'Previous'),
                        :next_label => I18n.t(:next), :default => 'Next'))
  end

alias_method_chain :will_paginate, :i18n

然后在config/locales/zh.yml中添加:

1
2
previous: "« 前一页"
next: "后一页 »"

findViewById()返回null

通过findViewById()获取一个Button时一直返回null值,百思不得其解。最后发现是因为在layout文件中使用了id的旧风格,如下所示:

1
<Button id="@+id/btn_okay" />

改成android:id就成功了。

1
<Button android:id="@+id/btn_okay" />

构建一个更友好的404页面

用户访问网站时偶尔会遇到404错误。404是一个HTTP状态码,告诉用户请求的网页不存在或链接错误。大多数网站托管和Web服务器会提供自己的默认错误页面,但如果开发者有提供定制的404页面,就可以增强用户的体验,对搜索引擎也更友好。定制的404页面非常非常有用,它不仅提供有关错误信息给用户,也会提供即时反馈给开发者,以便问题能被尽快修复;还可以告知搜索引擎链接已经失效,不要再索引。

这里是我对定制404页面的一些思考。

问题的分析

为了提供有用且特定的信息给用户,需要界定404错误可能的原因。按照页面访问的来源,错误大致可以分为以下4种:

  1. 用户拼错URL地址或者使用过期的书签。
  2. 站内的失效链接。
  3. 其它网站返回的失效链接。
  4. 搜索引擎返回的失效链接。

其中后面3种都是由于链接被修改、页面被删除或被移到其它地方所致。

问题的解决

为了确定404错误的来源,我们的代码需要访问HTTP Referer,它会告诉我们用户是从哪个页面链接过来的。以下是大概的代码执行步骤:

  1. 检测HTTP Referer以确定404错误的来源;
  2. 显示适当的消息给用户;
  3. 如果是特定的错误,发送邮件给开发者。

问题1:用户拼错URL地址或者使用过期的书签

在这种情况下,HTTP Referer通常为空,所以我们需要使用下面的代码做检查:

1
if request.referer.blank?

然后是定制404页面上的显示消息,告诉用户问题是什么:

1
2
3
4
5
很抱歉,你试图访问的页面 http://example.com/no-such-page.html 不存在。

看起来你似乎拼错了URL地址或者使用了过期的书签。

你或许应该试试搜索这个网站或者使用我们的站点地图去找到你想要的东西。

因为这是用户导致的错误,且没有问题要被修复,所以这里就不需要发送邮件给开发者。

问题2:站内的失效链接

当HTTP Referer不为空时,要检查它是指向本站、其它网站还是搜索引擎。如果包含本站域名,那么就可以知道用户是从站内其它页面过来的。这里使用以下的代码检测:

1
unless request.referer.index(request.host).nil?

然后,我们就可以给用户显示消息,告诉他该页面的链接失效了。

1
2
3
很抱歉,你试图访问的页面 http://example.com/no-such-page.html 不存在。

很明显,我们的页面上出现了失效链接。已经给开发者发送了邮件,问题将会很快被修复。你无需做任何事情。

并且,在显示消息给用户的同时发送包含所有必要信息的邮件给开发者。邮件的标题要清晰地指出哪个域名站点上有失效链接,邮件的内容要告诉开发者用户所在页面以及请求页面的URL地址。

1
2
3
4
5
6
7
8
9
From: example.com 404 error

Subject: 在站点 example.com 上有失效链接

Message: 站内的失效链接

在 http://example.com/badlink.html 页面上似乎存在失效链接。
有人正试图通过这个页面去访问 http://example.com/no-such-page.html 页面。
你应该去检查下该页面是否有什么问题。

问题3:其它网站返回的失效链接

如果404错误是由其它网站上的失效链接造成,那么可以显示以下的信息给用户:

1
2
3
4
5
很抱歉,你试图访问的页面 http://example.com/no-such-page.html 不存在。

很明显,在你过来的页面上存在失效链接。我们已经注意到了这个情况,并将试图去联系该页面的所有者修复它。

你或许应该试试搜索这个网站或者使用我们的站点地图去找到你想要的东西。

同时发送邮件给开发者,让开发者访问有失效链接的页面,如果页面上有所有者的联系信息,那么就可以通知他们修复这个问题。

1
2
3
4
5
6
7
8
9
From: example.com 404 error

Subject: 在站点 domain.com 上有失效链接

Message: 其它网站返回的失效链接

在 http://domain.com/badlink.html 页面上似乎存在失效链接。
有人正试图通过这个页面去访问 http://example.com/no-such-page.html 页面。
你应该联系页面的所有者去检查下该页面有什么问题。

问题4:搜索引擎返回的失效链接

为了确定用户是来自搜索引擎的结果页面,需要用一个搜索引擎URL列表来检测HTTP Referer,这个列表是一个排序的文本文件,这样我们可以在任何时候更新列表而不需要修改代码。这里是做检查的代码片段:

1
2
3
4
5
6
7
8
9
10
11
12
from_searchengine = false

IO.readlines('searchengines.txt') do |searchengine|
  unless request.referer.index(searchengine).nil?
    from_searchengine = true
    break
  end
end

if from_searchengine
  # do something
end

在这种情况下,我们要让用户知道搜索引擎返回的是旧链接:

1
2
3
4
5
很抱歉,你试图访问的页面 http://example.com/no-such-page.html 不存在。

看起来似乎是搜索引擎返回了旧页面的链接,等到搜索引擎更新索引后旧链接就会被移除。

你或许应该试试搜索这个网站或者使用我们的站点地图去找到你想要的东西。

因为没有什么我们可以做的,所以不需要发送邮件给开发者。

创建视图时的with check option选项

通过有with check option选项的视图操作基表,有以下结论:

  1. 首先视图只操作它可以查询出来的数据,对于它查询不出的数据,即使基表有,也不可以通过视图来操作;
  2. 对于update,有with check option,要保证update后,数据能被视图查询出来;
  3. 对于delete,有无with check option都一样;
  4. 对于insert,有with check option,要保证insert后,数据要被视图查询出来。

对于没有where子句的视图,使用with check option是多余的。

下面用一个例子来说明第4条:

1
2
3
4
5
6
7
8
9
10
11
12
CREATE TABLE students (
    id int NOT NULL AUTO_INCREMENT,
    name varchar(50) NOT NULL,
    age int,
    sex char(1),
    PRIMARY KEY (id)
);

CREATE VIEW male_students_view AS
    SELECT name, age FROM students
    WHERE sex = 'M'
    WITH CHECK OPTION;
1
2
-- 报CHECK OPTION failed错误,原因如4,插入数据后要还能被视图查询出来。
INSERT INTO male_students_view VALUES('WU', 18);
1
2
-- 报Column count doesn't match value count错误,因为视图中根本没有sex列。
INSERT INTO male_students_view VALUES('WU', 18, 'M');

将students.sex列改成默认值为M:

1
ALTER TABLE students MODIFY sex char(1) NOT NULL DEFAULT 'M'
1
2
-- 执行成功,1 row affected。
INSERT INTO male_students_view VALUES('WU', 18);
1
2
-- 还是同样的问题,Column count doesn't match value count。
INSERT INTO male_students_view VALUES('WU', 18, 'M');

或者不修改students.sex的属性,将视图改成:

1
2
3
4
CREATE VIEW male_students_view AS
    SELECT name, age, sex FROM students
    WHERE sex = 'M'
    WITH CHECK OPTION;
1
2
-- 会报CHECK OPTION failed错误。
INSERT INTO male_students_view VALUES('WU', 18);
1
2
-- 执行成功,1 row affected。
INSERT INTO male_students_view VALUES('WU', 18, 'M');

以上例子都在MySQL上实际执行过。