class NetworkVideoDevice < ActiveRecord::Base
require 'ping'
def responds_to_ping?
Ping.pingecho host, 1.0
end
has_many :live_streams
validates_uniqueness_of :host
validates_presence_of :name
named_scope :ordered_by_host, {:order => "host"}
end
class NetworkAudioDevice < ActiveRecord::Base
require 'ping'
def responds_to_ping?
Ping.pingecho host, 1.0
end
has_many :live_streams
validates_uniqueness_of :host
validates_presence_of :name
named_scope :ordered_by_host, {:order => "host"}
end
At this point, developers may be aware of the code smells. Maintaining the same functionality in two places is error-prone, hard to debug, and puts an unnecessary burden on developers. In the pre-Rails 2.0 days, I saw many developers using Single Table Inheritence to share functionality between models. As Rails developers' familiarity with Ruby increased, though, I've noticed developers taking advantage of Ruby's Modules for solutions to problems like this.
First, let's factor out the common method, responds_to_ping? into a module. Since this module will only be used by Active Record models, I'm going to put it directly in the app/models directory so I don't even have to require it anywhere. Now, the code looks like this:
# the newly-created module:
module NetworkDevice
require 'ping'
def responds_to_ping?
Ping.pingecho host, 1.0
end
end
class NetworkVideoDevice < ActiveRecord::Base
include NetworkDevice
has_many :live_streams
validates_uniqueness_of :host
validates_presence_of :name
named_scope :ordered_by_host, {:order => "host"}
end
class NetworkAudioDevice < ActiveRecord::Base
include NetworkDevice
has_many :live_streams
validates_uniqueness_of :host
validates_presence_of :name
named_scope :ordered_by_host, {:order => "host"}
end
Now, if we want to change that ping timeout from one second to two seconds, we only need to change it in one place.
This is a good start, but we can take our refactoring even further.
Sharing ActiveRecord Functionality via included and class_eval
We also do the same validation on some fields, and we have the same named_scope call in both models. Let's stick those in the module, too! Now, NetworkDevice looks like this:
module NetworkDevice < ActiveRecord::Base
require 'ping'
def responds_to_ping?
Ping.pingecho host, 1.0
end
has_many :live_streams
validates_uniqueness_of :host
validates_presence_of :name
named_scope :ordered_by_host, {:order => "host"}
end
NoMethodError: undefined method `has_many' for NetworkDevice:Module
Our NetworkDevice module doesn't know anything about ActiveRecord methods like has_many, so when Ruby tries to evaluate it outside the context of an AR::Base-descended class, it doesn't work.
The solution? use Module's included method and Class's class_eval method.
included, a class method available to modules, lets us defer funcionality until the module is included somewhere. We simply tell Ruby to evaluate some code in the context of the including class, which will descend from ActiveRecord::Base.
class_eval lets us define or call class methods on the receiver on the fly. In this case, we'll want to call some ActiveRecord::Base class methods, like named_scope.
Sound confusing? Let's see the new code in action:
module NetworkDevice < ActiveRecord::Base
require 'ping'
def responds_to_ping?
Ping.pingecho host, 1.0
end
# run this in the context of the including class:
def self.included(base_class)
base_class.class_eval do
has_many :live_streams
validates_uniqueness_of :host
validates_presence_of :name
named_scope :ordered_by_host, {:order => "host"}
end
end
end
No comments:
Post a Comment