Rubyisms: forwardables

Written on 6:23:00 PM by S. Potter

Recently some Java friends of mine have decided to taste the juicier fruits in Ruby-land with my assistance. So below are some excerpts from an email conversation I had with one about Ruby's standard library forwardable features:

Suppose we have the following model classes defined for a simple CRM system: Customer, Address, PhoneNumber, Name, etc. Now in Java-land we would have written something that looks like the following Ruby code (except you must type about a hundred more lines - although now you can write annotations to automate basic things like generate getters and setters or the like, which still requires a ridiculous amount of code to accomplish):


Name = Struct.new(:first_name, :last_name, :suffix, :salutation)
Address = Struct.new(:street_address, :suite, :city, :state, :country, :zip)
PhoneNumber = Struct.new(:country_code, :area_code, :exchange, :number, :extension)

class Customer
   attr_accessor :name, :address, :phone_number
end

Note: the attr_accessor will create getters and setters for Ruby newbies.


The problem is that you are not sure when you first design this if it will support future product offerings for business customers, which means the way we have modeled our simple CRM above may not support a business customer as well as individual customers. However, as good agile developers we know better than to pre-empt future requirements by modeling the world up-front, so we stick with our current implementation for this next iteration/release. To help us with future changes (even though we do not know what they will be or try to pre-empt them) we want to be able to access details directly from the Customer object to reduce coupling of clients of our Customer model. This also helps us implicitly adhere to the Law of Demeter (when not misused). A naive approach would be to manually create accessor methods for the Address, Name and PhoneNumber attributes in the Customer class. But remember, this is Ruby-land, which is a magical place, so we can just do the following:


require 'forwardable'
NAME_FIELDS = [:first_name, :last_name, :suffix, :salutation]
ADDRESS_FIELDS = [:street_address, :suite, :city, :state, :country, :zip]
PHONE_NUMBER_FIELDS = [:country_code, :area_code, :exchange, :number, :extension]
Name = Struct.new(*NAME_FIELDS)
Address = Struct.new(*ADDRESS_FIELDS)
PhoneNumber = Struct.new(*PHONE_NUMBER_FIELDS)

class Customer
   extend Forwardable
   attr_accessors :name, :address, :phone_number
   def_delegators :@name, *NAME_FIELDS
   def_delegators :@address, *ADDRESS_FIELDS
   def_delegators :@phone_number, *PHONE_NUMBER_FIELDS
   def initialize(name, address, phone_number)
     @name, @address, @phone_number = name, address, phone_number
   end
end

One of the many fantastic things about Ruby (and Python) is we have irb Ruby's interactive shell to prototype with. So we open up a irb shell and do the following:



 irb> require 'FILE_WITH_ABOVE_CODE_IN'
 irb> john_adams = Customer.new(Name.new('John', 'Adams', nil, 'Former President'), Address.new('101 Constabulary Road', nil, 'Bay Colony', 'MA', 'US', '000000'), nil) # remember phones didn't exist in the mid-18th Century! => #<Customer:0xb7f2be30 @address=#<struct Address street_address="101 Constabulary Road", suite=nil, city="Bay Colony", state="MA", country="US", zip="000000">, @name=#<struct Name first_name="John", last_name="Adams", suffix=nil, salutation="Former President">, @phone_number=nil> 
 irb> john_adams.salutation => "Former President"
 irb> john_adams.state => "MA"
 irb> john_adams.street_address => "101 Constabulary Road"

Javahead>>> We are still passing in the Name, Address and PhoneNumber stucts to create the Customer object in the first place, so why would flattening out the Customer interface reduce coupling?

Great question [Javahead]! In general, most applications will read data from models much more than create them, and quite a number of applications do not actually initially create any of the data, they simply massage it. Now I am not suggesting that there are no applications that would create model objects more than read from them, but most likely your application will be 80% reading and 20% writing (including creating and updating). So if we were to change the underlying structure of the Customer, on average 80% of the code that uses the Customer object would not know about its constituents and would not need to be changed.

Of course, this is somewhat of a contrived example, but I hope it shows how concise, simple, and yet powerful Ruby's forwardables can be, but power comes at a cost: potential and ease of misuse! Enjoy.

If you enjoyed this post Subscribe to our feed

No Comment

Post a Comment