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.
Nested object path decoupling
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.
A naive approach would be to manually create accessor methods for the
Address
, Name
, 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
Ruby Forwardables in the interactive shell
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 an
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 elegant, concise, simple, and yet powerful Rubyisms can be.
If you enjoyed this content, please consider sharing this link with a friend, following my GitHub, Twitter/X or LinkedIn accounts, or subscribing to my RSS feed.