Needle/Rails, Injected - Interceptors - Attn: Jamis Buck

J

John Wilger

Jamis (or anyone else who cares to give it a go),

I'm having a bit of trouble wrapping my head around the usage of
interceptors with Needle. I've implemented the changes you outlined in
you code examples from you proposal on "Rails, Injected"
(http://ruby.jamisbuck.org/rails-injected.html) and would like to use
the method interception within a few of my ActiveRecord classes.
Specifically, I have the following (abbreviated) classes:

-- snip --

class Project < ActiveRecord::Base
has_many :story_cards

def status
ProjectStatus::find(self.status_name)
end

def status=(new_status)
if ProjectStatus.include?(new_status)
self.status_name = new_status.name
else
raise ArgumentError, "Only predefined instances of ProjectStatus
can be assigned."
end
end
end

class StoryCard < ActiveRecord::Base
belongs_to :project
end

-- end snip --

And I have RankedValue/ProjectStatus defined as:

-- snip --

module RankedValue
include Comparable

attr_reader :name, :rank

def initialize(name, rank)
@name = name
@rank = rank
end

def self.included(mod)
mod.module_eval <<-EOF,__FILE__,__LINE__
def self.new(*args, &block)
name = args[0]
name = name.upcase.gsub(' ', '_')
const_set(name, super(*args, &block))
end
private_class_method :new

def self.to_a
constants.collect! { |cnst| const_get(cnst) }
end

def self.include?(obj)
self.to_a.include?(obj)
end

def self.find(name)
begin
name_trans = name.upcase.gsub(' ', '_')
const_get(name_trans)
rescue NameError
raise "No predefined instance of #{mod} matches #name == '\#{name}'."
end
end
EOF
end

def <=>(other)
self.rank <=> other.rank
end
end

class ProjectStatus
include RankedValue

def initialize(name, rank, closed)
super(name, rank)
@closed = closed
end

def is_closed?
@closed
end

new('Pending', 1, false)
new('In Progress', 2, false)
new('On Hold', 3, true)
new('Completed', 4, true)
new('Cancelled', 5, true)
end

-- end snip --

So, basically, I have the following instances of ProjectStatus available:

* ProjectStatus::pENDING
* ProjectStatus::IN_PROGRESS
* ProjectStatus::ON_HOLD
* ProjectStatus::COMPLETED
* ProjectStatus::CANCELLED

each of which responds to #is_closed? with true or false depending on
how the instance was initialized.

OK, now that we're through all the background (whew!), here's what I
want to be able to do. I want to make sure that a StoryCard can only
be assigned to a Project when Project#status.is_closed? == false.

I know that I could implement this by overriding the
StoryCard#project_id= method, but this seems to break encapsulation.
It should ultimately be the Project class's responsibility to decide
if a StoryCard can be assigned to it. I'm thinking I could create an
interceptor within the Project class definition to intercept the calls
to StoryCard#project_id= and add the necessary advice before the
assignment takes place (and raise a RuntimeError if the Project is
closed), but I'm not sure where to start.

Any advice?

--
Regards,
John Wilger

-----------
Alice came to a fork in the road. "Which road do I take?" she asked.
"Where do you want to go?" responded the Cheshire cat.
"I don't know," Alice answered.
"Then," said the cat, "it doesn't matter."
- Lewis Carrol, Alice in Wonderland
 
J

Jamis Buck

John said:
I know that I could implement this by overriding the
StoryCard#project_id= method, but this seems to break encapsulation.
It should ultimately be the Project class's responsibility to decide
if a StoryCard can be assigned to it. I'm thinking I could create an
interceptor within the Project class definition to intercept the calls
to StoryCard#project_id= and add the necessary advice before the
assignment takes place (and raise a RuntimeError if the Project is
closed), but I'm not sure where to start.

Well, I'm not exactly an expert in AR, so I'll probably be making some
assumptions that are false.

First of all, I'm assuming StoryCard#project_id= takes an integer and
not a Project instance.

This means that the interceptor would not have access to the project
instance, unless you did a look up in the interceptor itself. So, let's
attempt that.

In your 'project.rb' file, do something like this:

# do this first so that we can guarantee that the story_card service
# will have been registered.
require 'storycard'

class Project < ActiveRecord::Base
registry.intercept( :story_card ).doing do |chain,context|
# only intercept the #project_id= method
if context.sym == :project_id=
# use the registry, not the Project constant, so that we
# if interceptors are added to the project service, they
# will be invoked.
project = registry[ :project ].find( context.args.first )
if project.status.is_closed?
raise "cannot add storycard to a closed service!"
else
chain.process_next( context )
end
else
chain.process_next( context )
end
end

...
end

That said, I'm not confident that this approach will work, and here's
why: I believe that when you assign a storycard to a project, the
#project_id= call occurs internally, within the StoryCard class itself.
That means that the interceptors are bypassed (interceptors are only
invoked when a client invokes a method on the service--methods invoked
internally by the service do not go through the interceptors).

Also, the AR objects use the constant names internally to reference
other AR classes, which means that they aren't going through the
registry anyway to get the dependencies. (Which means interceptors won't
help you at all, in that case.)

This all goes back to why I don't, in general, like giving classes
themselves prominant acting roles. Class methods are just globals, and
like all globals should be used sparingly. DI canhelp reduce the
necessity of class methods, but only if the framework was designed with
DI in mind.
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

No members online now.

Forum statistics

Threads
473,995
Messages
2,570,236
Members
46,825
Latest member
VernonQuy6

Latest Threads

Top