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
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
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
PhoneNumber attributes in the
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 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
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.