Recently I had a requirement on an app that had 2 types of groups.
- public
- private
Anyone can join a public group.
However to join a private group you have to be registered on the private group’s website, e.g. pretend Yahoo is a private group, now in order to join the Yahoo group you have to have an account with Yahoo.
Let’s pretend that every private group’s website implements a simple API that says if the given username/password is registered with that website.
I’m going to write a client for the API and put it in a module in lib
.
In lib/api_client.rb
:
module ApiClient
def authenticate(username, password)
# make HTTP GET request to the private group's
# website's API implementation URL
# true/false return values
end
end
I’ll use this library in my Membership
model during a validation callback on
create.
class Membership < ActiveRecord::Base
belongs_to :user
belongs_to :group
include ApiClient
attr_accessor :username, :password
def validate_on_create
unless authenticate(username, password)
errors.add_to_base "You are not registered with this group's website"
end
end
end
Here we mix in the ApiClient
module and use its #authenticate
method to
validate this membership’s username and password (username and password were
added as virtual attributes using attr_accessor
since I don’t want to store
this remote site’s credentials in my apps db).
The problem I have with this implementation is that the #authenticate
method
that gets mixed into Membership
via ApiClient
is now a public instance
method on Membership
and can therefore be used anywhere. But #authenticate
should only be used during validation during creation.
membership = Membership.find :first
membership.authenticate
That’s no good. It’s polluting my Membership
public interface.
Let’s try something else in our Membership
model.
class Membership < ActiveRecord::Base
belongs_to :user
belongs_to :group
attr_accessor :username, :password
def validate_on_create
api_client = Object.new
api_client.extend ApiClient
unless api_client.authenticate(username, password)
errors.add_to_base "You are not registered with this group's website"
end
end
end
Here I created an instance of Object
, mixed ApiClient
in just that instance
and then used it to #authenticate
the username/password. This is an example
of per-object behavior and it gives me exactly what I want. I don’t pollute the
public interface of Membership
and yet still get to have my remote API behavior all in one place,
ApiClient
.
Can we shorten #validate_on_create
a little more?
def validate_on_create
api_client = returning(Object.new) {|instance| instance.extend ApiClient}
unless api_client.authenticate(username, password)
errors.add_to_base "You are not registered with this group's website"
end
end
Oh man, an excuse to use the method all the cool kids are using, #returning
.
I might keep that. As long as it doesn’t go longer than my 80-column character
limit but its already starting to push it!
The final implementation of this feature is an example of per-object behavior. A style similar to coding in JavaScript.
When the Rails guys tried to make JavaScript more Ruby-like in their prototype
library they added a method to the Object constructor function named #extend
to do the very same thing as Ruby’s Object#extend.
The above example might look like this in JavaScript.
var ApiClient = {
authenticate : function () {
// make XML HTTP GET request to the private group's website
// API implementation URL
// true/false return values
};
};
function validateOnCreate () {
var apiClient = new Object();
Object.extend(apiClient, ApiClient);
if (! apiClient.authenticate()) {
// add/display errors
}
}
David A. Black recently spoke about this at RubyConf 2007. In his slides he mentions how most Ruby programmers already use it when using class methods and how its also useful as a way to avoid changing core Ruby. Dave Thomas mentioned per-object or OOP without classes at RailsConf 2007 as well.
Both of these guys got my interest in removing classes from OOP; before them all I’d seen of this technique was the language Self and its descendant JavaScript. Anyway enough bs, its an interesting technique and one I’m going to monitor to see if I see myself moving more and more in this direction.