Agile Web Development with Rails 翻译(四十七)
Agile Web Development with Rails 翻译(四十七)
这儿有两种基本的回调实现方式。
首先,你可以直接地定义回调实例方法。例如,如果你想在保存事件之前处理,你可以写
class Order < ActiveRecord::Base
# ..
def before_save
self.payment_due ||= Time.now + 30.days
end
end
第二种定义回调的基本方法是声明处理器。一个处理器即可以是个方法也可以是个块[处理也可以被包含在字符串中,用eval()计算,但是这不被推荐。]。你在事件后面使用类方法的名字来将一个特定的事件与一个处理器关联起来。要关联一个方法,声明它为private或protected并指定它的名字做为一个符号给处理器声明。要指定一个块,在声明后简单添加它。这个块接受“模型”对象做为一个参数。
class Order < ActiveRecord::Base
before_validation :normalize_credit_card_number
after_create do |order|
logger.info "Order #{order.id} created"
end
protected
def normalize_credit_card_number
self.cc_number.gsub!(/-w/, '')
end
end
你可以为同一个回调指定多个处理器。它们通常按它们被指定的次序来调用,除非一个处理器返回false(它必须是实际的false值),在这种情况下,回调链被提前中止。
出于性能优化的原因,对于after_find和after_initialize事件,只有一种方式定义“回调”,即把它们定义成方法。如果你试图使用第二种技术声明它们为处理器,它们将默默地被忽略。
Timestamping Records
before_create和before_update回调有种潜在的用法是timestamping行。
class Order < ActiveRecord::Base
def before_create
self.order_created ||= Time.now
end
def before_update
self.order_modified = Time.now
end
end
“活动记录”可以不让你操心这些事。如果你的数据库表有个列名为created_at或created_on,它会自动地设置行创建时间的时间戳(timestamp)。同样地,一个列名字为updated_at或updated_on将设置最后修改的时间戳。这些时间戳缺省地是本地时间;要使用UTC(或GMT),在你的代码包括下面行(即可以内联独立的“活动记录”应用程序,也可以用在完整的Rails应用程序的环境文件中)。
ActiveRecord::Base.default_timezone = :utc
可以像下面这样取消它
ActiveRecord::Base.record_timestamps = false
Callback Objects
可在“模型”类内直接指定回调处理器,你可以创建分离的处理器类,它封装所有回调方法。这些处理器可以在多个“模型”间共享。一个处理器类是个简单的类,它定义回调方法(before_save(),after_create(),等等)。在app/models目录内为这些处理器类创建源文件。
在“模型”对象内使用处理器,你创建这个处理器类的实例,并传递那个实例给各种回调定义。几个例子会让这些更清楚些。
如果我们应用程序在多个地方使用信用卡,我们可能想在多个方法内共享我们的normalize_credit_card_number()方法。要做到这点,我们要抽取方法到它自己的类中并在我们希望它处理的事件名后命名它。这个方法将接受单个参数,回调被生成的“模型”对象。
class CreditCardCallbacks
# Normalize the credit card number
def before_validation(model)
model.cc_number.gsub!(/-w/, '')
end
end
现在,在我们的“模型”类中,我们可以重排这个被调用的共享的回调。
class Order < ActiveRecord::Base
before_validation CreditCardCallbacks.new
# ...
end
class Subscription < ActiveRecord::Base
before_validation CreditCardCallbacks.new
# ...
end
这个例子中,处理器类假设信用卡号码被保存在一个“模型”属性cc_number内,Order和Subscription两者将有一个与之同名的属性。但是我们可以推广这个思想,让处理器类减少对使用它的类的实现细节的依赖。
例如,我们可以创建一个普通的加密和解密处理器。这可以在它们被存储到数据库之前加密名字字段,并在行被读回时解密它们。你可以做为一个回调处理器包括它在任何需要此功能的“模型”内。
处理需要在“模型”数据被写回数据库之前,加密一个“模型”内给出的属性集。因为我们的应用程序需要处理这些属性的文本文件,完成存储之间它重新排列及解密它们。它也需要在行被从数据库读入到一个“模型”对象时解密这些数据。这些要求意味着我们需要在保存数据和找到一个新行时解密数据库行,我们可以通过别名after_find()方法为after_save()—同一个方法有两个名字,来保存代码。
class Encrypter
# We're passed a list of attributes that should
# be stored encrypted in the database
def initialize(attrs_to_manage)
@attrs_to_manage = attrs_to_manage
end
# Before saving or updating, encrypt the fields using the NSA and
# DHS approved Shift Cipher
def before_save(model)
@attrs_to_manage.each do |field|
model[field].tr!("a-z", "b-za")
end
end
# After saving, decrypt them back
def after_save(model)
@attrs_to_manage.each do |field|
model[field].tr!("b-za", "a-z")
end
end
# Do the same after finding an existing record
alias_method :after_find, :after_save
end
我们现在可以重排从我们的orders“模型”内调用的Encrypter类。
require "encrypter"
class Order < ActiveRecord::Base
encrypter = Encrypter.new(:name, :email)
before_save encrypter
after_save encrypter
after_find encrypter
protected
def after_find
end
end
我们创建一个新的Encrypter对象并用它钩住before_save,after_save,和after_find事件。这种方式,在一个定单被保存之前,encrypter内的方法before_save()将被调用,等等。
那么,我们为什么要定义一个空的after_find()方法呢?记住我们说过,出于性能的原因after_find和after_initialize被视为是特别的。这种特殊对待的结果之一是“活动记录”不想知道调用一个after_finde处理器,除非它在“模型”类中看到一个真实的after_find()方法。我们必须定义一个空的占位符给after_find处理。
This is all very well, but every model class that wants to make use of our encryption handler would need to include some eight lines of code, 就像我们在Order类内做的那样。我们会做的更好,我们将定义一个帮助方法,它完成所有工作,并且让那个帮助方法对所有“活动记录模型”都是有效的。要做到这些,我们将添加它到ActiveRecord::Base类中。
class ActiveRecord::Base
def self.encrypt(*attr_names)
encrypter = Encrypter.new(attr_names)
before_save encrypter
after_save encrypter
after_find encrypter
define_method(:after_find) { }
end
end
像这样,我们现在可以使用单个调用来添加encryption给任何“模型”类的属性。
class Order < ActiveRecord::Base
encrypt(:name, :email)
end
一个简单的驱动程序让我们来检验这个。
o = Order.new
o.name = "Dave Thomas"
o.address = "123 The Street"
o.email = "dave@pragprog.com"
o.save
puts o.name
o = Order.find(o.id)
puts o.name
在控制台,我们在“模型”对象内看到我们的消费者的名字。
ar> ruby encrypt.rb
Dave Thomas
Dave Thomas
但是,在数据库中,很明显名字和email地址被加密了。
ar> mysql -urailsuser -prailspw railsdb
mysql> select * from orders;
+----+-------------+-------------------+----------------+----------+--------------+
| id | name | email | address | pay_type | when_shipped |
+----+-------------+-------------------+----------------+----------+--------------+
| 1 | Dbwf Tipnbt | ebwf@qsbhqsph.dpn | 123 The Street | | NULL |
+----+-------------+-------------------+----------------+----------+--------------+
1 row in set (0.00 sec)
Observers
回调是个很好的技术,但它们可能有时候会导致一个“模型”类会接受与其本意并不相关的职责。例如,在266页我们创建一个回调。
当一个定单被创建时,一个消息被日志。那个功能并不真是基本Order类的一部分—我们把它放在那里是因为回调在那里执行。
“活动记录”的“观察者”(observer)克服了那个限制。一个“观察者”显示地连接本身到一个“模型”类中,为了回调注册它自己为“模型”的一部分,但是并不要求“模型”本身做任何更改。这儿是先前我们logging例子,我们用“观察者”重写了它。
class OrderObserver < ActiveRecord::Observer
def after_save(an_order)
an_order.logger.info("Order #{an_order.id} created")
end
end
OrderObserver.instance
在ActiveRecord::Observer 被子类化时,它查看新类名字,从尾部剥去单词Observer,并且假设左侧是被观察的“模型”类的名字。在我们的例子中,我们称我们的“观察者”类为OrderObserver,所以它自动地把它自己同“模型”Order钩住。
有时候就不会这么方便。当它执行时,“观察者”类可能使用observer()方法明确地列出了它想观察的“模型”。
class AuditObserver < ActiveRecord::Observer
observe Order, Payment, Refund
def after_save(model)
model.logger.info("#{model.class.name} #{model.id} created")
end
end
AuditObserver.instance
在这两个例子中,我们必须创建一个“观察者”实例—只定义“观察者”类是不能完成观察的。对标准的“活动记录”应用程序来说,你还需要初始化期间在某些方便的地方调用instance()方法。如果你写一个Rails应用程序,你将在你的ApplicationController内使用observer指令,就像我们在278页看到。
习惯上,observer源文件放在app/models目录内。
在某种程序上,“观察者”会为Rails带来些first-generation-aspect-oriented程序的好处如,Java。它们允许你注入行为到“模型”类中,而不用修改这些类内的任何代码。

浙公网安备 33010602011771号