1.
在上一篇文章中,我们介绍了如何使用edit field来编辑日期和时间,而不是用很多的dropdown menus. 那些日期和时间首先被用文本输入到一个编辑框中,然后被rails用自带的日期解析函数解析成日期和时间存储到数据库中。
例如:
Task name: Task 1
Due at time string:
2009-08-20 08:00:00
为了完成这个任务,我们为Task model创建了一个虚拟属性,叫做due_at_string, 这个属性具有 get method 和 set method, get method 从数据库中读取due_at 字段,然后以某种格式的字符串显示出来, set method则解析用户输入的字符串,将其转化成日期时间类型,并存储到数据库的due_at 字段中, 具体代码如下:
def due_at_string
due_at.to_s(:db)
end
def due_at_string=(due_at_str)
self.due_at = Time.parse(due_at_str)
rescue ArguementError
@due_at_invalid = true
end
这种为某一个属性单独写set method 和 get method的做法处理某一个属性当然可以,但是如果需要单独写set和get 的属性很多呢,例如还有许多其他字段也是日期时间格式?我们代码会有很多的重复。
为了解决这个问题,我们的方法是添加一个类方法,名字叫stringify_time。
这个方法的作用就是动态的为每一个传给他的属性自动创建getter和setter方法。cool把
而且可以预计到,这个方法我们在其他的项目中也会经常用到,因此我们要把他封装成一个plugin。呵呵
2. Create Plugin:
首先,我们要generate 一个空的plugin 名字叫 stringify_time
命令如下:
script/generate plugin stringify_time
这个命令将在我们的工程的vender/plugins文件夹生成一个新的叫stringify_time的文件夹
我们先来看看init.rb文件, 这个是初始化文件,它将在plugin 被load in 之前load 进来,因此,这里我们要在init.rb 中添加如下一行:
这行代码将会把lib目录下的stringify_time.rb 文件load 进来。
require 'stringify_time'
而在lib/stringify_time.rb 文件中,这里是主要代码所在地,我们将在这里添加类似的getter 和 setter methods, 我们首先要定义一个module,其中含有stringify_time 方法。
module StringifyTime
def stringify_time(*names)
end
end
stringify_time这个方法将接收多个字段名字作为参数,这里我们用*names,表示这个方法可以接收多个参数,所有参数构成一个叫names的数组供方法内部调用,这个是个很有用的小知识,一般人我不告诉他哦!
在方法内部,循环使用数组中的名字,使用define_method这个方法,动态的创建名字叫[name]_string 的方法,那么如果我们传递due_at给这个方法,我们就会得到due_at_string方法。
define_method 这个方法接收一个block,block内的代码就是动态定义的方法的内容,以之前的due_at_string方法为例,这个方法从数据库中读取due_at字段,然后将其日期时间类型转换成一个字符串返回,但是这里不能用dua_at 这个名字了,只能用read_attribute 方法获得字段:
def stringify_time(*names)
names.each do |name|
define_method "#{name}_string" do
read_attribut(name).to_s(:db)
end
end
end
就是这么简单,我们就创建了很多属性的getter方法。
同样的,我们照葫芦画瓢写出动态创建setter方法的类方法
names.each do |name|
define_method "#{name}_string=" do |time_str|
begin
write_attribute(name, Time.parse(time_str))
rescue ArgumentError
instance_variable_set("@#{name}_invalid", true)
end
end
end
哇噻,太牛逼的程序了,可惜不是我写的,嘿嘿!!!!
可以看到,在setter method 中,我们仍然使用了define_method方法,但是要setter method要接收参数,这个参数可以放在随后的block中.
全部代码如下:
module StringifyTime
def stringify_time(*names)
names.each do |name|
define_method "#{name}_string" do
read_attribute(name).to_s(:db)
end
define_method "#{name}_string=" do |time_str|
begin
write_attribute(name, Time.parse(time_str))
rescue ArgumentError
instance_variable_set("@#{name}_invalid", true)
end
end
end
end
end
稍等,我们已经写好了plugin,但是在程序中如何让没有active record model自动具有这个pulgin的功能呢,这就需要将这个plugin中的的module extend到ActiveRecord::Base这个类中,这个工作要放在init.rb中:
class ActiveRecore:Base
extend StringifyTime
end
注意我们用的是extend而不是include, extend的作用是将module中的方法作为类方法而不是实例方法添加到ActiveRecord::Base中,因为我们的stringify_time是类方法,而不是实例方法,因为这个方法的运行不需要有任何对象。
而include 将会把StringifyTime中的方法作为实例方法放入到ActiveRecord::Base中。
3. 既然我们已经制作了plugin,而且把他包含进了ActiveRecord::Base类中,那么接下来就是如何使用他了,在我们的Task模型中:
class Task < ActiveRecord::Base
belongs_to :project
stringify_time :due_at
def validate
errors.add(:due_at, "is_invalid") if @due_at_invalid
end
end
4. 完善:
我们发现在validate方法中,我们用@due_at_invalid这个实例变量来判断,但是这个实例变量是在plugin中定义的,这里突然冒出来,后面的开发者很可能不知道他是哪里来的,因此我们最好使用方法而不是直接使用实例变量:
def validate
errors.add(:due_at, "is_invalid") if due_at_invalid?
end
而这个方法的定义自然也应该放在plugin中:
define_method "#{name}_invalid?" do
return instance_variable_get("#{name}_invalid")
end
至此,我们终于完成了一个plugin的制作。
如果你要在其他的项目中复用这个plugin,只要把这个目录拷贝到该项目的vender/plugin目录下即可,很简单把,嘿嘿。
5. 再说一点题外话:
关于block,例如
class File
def File.open_and_process(*args)
f = File.open(*args)
yield f
f.close
end
end
File.open_and_process('testfile', 'r') do |file|
while line=file.gets
puts line
end
end
这就是一个 后面可以跟着block的方法的定义和使用的全过程。