[LRUG] Ruby Cluetrain
James Adam
james at lazyatom.com
Mon Oct 26 07:11:39 PDT 2009
On 26 Oct 2009, at 13:34, Anthony Green wrote:
> I hope James won't mind me posting this Gist he tweeted at the
> weekend. But
> it exemplifies the kind of gaps I have in my Ruby knowledge.
> Whilst I understand what individual segments of code are doing I don't
> understand what the code as a whole is attempting to do.
>
> Anyone willing to shed some light ?
>
> http://gist.github.com/217001
I can provide a bit more context; the gist was a pared-down example of
the actual problem I was considering. Perhaps the real problem with
make clear what I was trying to do, and why the error was significant.
I was/am writing an API library to access my companies' FreeAgent
invoice data, and as part of this I wanted access to multiple
companies within the same process. I decided to use ActiveResource as
the basis for this, but that ties each class to a single API endpoint.
In other words, if the Project class is set up to load from company1.freeagentcentral.com
, then you'll need to create a different class (e.g. AnotherCompany)
to access projects from company2.freeagentcentral.com.
<plug shameless="true">
Btw, I really like FreeAgent - if you are interested in that sort
of thing, sign up using my referral code :-) http://www.freeagentcentral.com/?referrer=31h0wcs9
</plug>
Anyway, to the code. So my idea was to be able to dynamically create
the API classes within their own namespace. Something like this:
CompanyOne = FreeAgent::API.for(:site => "company1", :login => "bob")
CompanyTwo = FreeAgent::API.for(:site => "company1", :login => "bob")
# so now exists:
CompanyOne::Project, CompanyOne::Contact, CompanyOne::Invoice # and so
on, all reading data from https://company1.freeagentcentral.com
CompanyTwo::Project # etc, all reading data from https://company2.freeagentcentral.com
If you're really curious, the FreeAgent::API#for method is actually
creating a new module (`Mod = Module.new`), creating new classes
within that module (`sibling = Class.new(Base); Mod.const_set
(:Sibling, sibling)`), and including shared behaviour in those classes
(`sibling.class_eval { include Behaviour }`). This is what I was
trying to mirror in the gist linked above (so hopefully MRJ see that I
wasn't using Module.new for shits and giggles!)
It's the including of Behaviour that reveals the issue. In the
'Behaviour' module, I'm refering to another class that will be
generated in the module. In my FreeAgent library, it's something like
this
module FreeAgent
module ProjectBehaviour
def contacts
Contact.find(:all, :params => {:project_id => self.id})
end
end
end
CompanyOne::Project has this module included into it (as does
CompanyTwo::Project), and so when the method runs, I wanted 'Contact'
to actually refer to CompanyOne::Contact (or CompanyTwo::Contact
correspondingly).
The problem is, however, that in Ruby, constant lookup doesn't work in
the same way as method lookup. I'm sure others will be able to explain
this better than I can, but in a nutshell, when you refer to a
constant, it is bound using the lexical scope (i.e. something like
what's nearest as the source is being parsed). This means that the
reference to Contact in FreeAgent::ProjectBehaviour is actually going
to try and look for a Contact constant in FreeAgent:ProjectBehaviour,
or, I think, FreeAgent (again, the hairy details here are best
explained by someone else using a more trivial example), but certainly
not in the CompanyOne module since it does not exist while Ruby is
parsing the source code.
I hope that makes some sense, and reveals the intention of the code.
It's a "problem" that can be solved in a few different ways; I chose
to remove the references to the Contact constant, and use a method to
lookup the constant at runtime instead.
However, it's worth noting that the library I have ended up with is at
*least* twice as complex as it was before I hit this issue, and it all
stems from the original desire to want to dynamically compose API
namespaces for multiple companies on the fly. Had I been happy to
access a single company's details at a time, I could've just built a
simple FreeAgent::Project/Contact/Invoice set of classes, and avoided
the whole issue.
What fun! At least I learned something new about Ruby :)
- James
P.S. Another way of viewing this is as a demonstration of the problems
when tying configuration to class definition; if ActiveResource
configuration applied to instances, rather than classes, I could've
avoided this too. For example, rather than:
class Project < ActiveResource::Base
self.site = "http://api.com"
end
class OtherProject < ActiveResource::Base
self.site = "http://otherapi.com"
self.element_name = "project"
end
Project.find(:all) # => [<Project...>, <Project...>, ...]
OtherProject.find(:first) # => <Project...>
we could have:
class ProjectAPI < ActiveResource::Base
end
api1 = ProjectAPI.new(:site => "http://api.com")
api2 = ProjectAPI.new(:site => "http://otherapi.com")
api1.find(:all) # => [<Project...>, <Project...>, ...]
api2.find(:first) # => <Project...>
I'm torn as to which is actually nicer to use in practice, but I'm
fairly confident that the latter is "more OO".
P.P.S http://www.freeagentcentral.com/?referrer=31h0wcs - yes, I am
shameless. It's a Rails site too, so you basically *have* to support
it! :)
More information about the Chat
mailing list