[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