This is Part III in a series of posts addressing the problems and solutions that I have encountered while building an eCommerce application. Part I and Part II are available here.
For everyone who’s been following along, we’ve already built a Payment
class and explored various ways of “monetizing” the class. We’ve built our own accessor methods, and used ActiveRecord::Base
‘s composed_of
method.
We’re now at the point where we may have multiple classes that will all be dealing with money, and we’re looking for something a little less cumbersome.
Rails Magic
I still distinctly remember the first time I used Rails and I saw how easy it was to define relationships between models. The first time I used has_many
, I was hooked; even if I didn’t know how it worked.
So, today, we’re going to define our own method monetize
that will do all of the work that we did in our first two posts.
Our end goal will look like this:
# app/models/payment.rb
class Payment < ActiveRecord::Base
monetize :amount
end
Very succinct, but I also believe very expressive. I think that most people dipping into this code will understand that the amount
method will return some sort of “monetized” result. This is much cleaner than defining our own getters and setters, or even using the composed_of
method which can be a little confusing.
They’re Just Class Methods!
The day that I realized that has_many
, belongs_to
, and acts_as_list
were just class methods, was when I realized the power of being able to define my own.
So, let’s go ahead and define self.monetize
that we can call on our payment class. As you’ll quickly see, I’ve made the decision to define my “monetized” methods using our original approach of having getters and setters. I could just as easily dynamically added composed_of
blocks of code, but I’d like this to work on any Ruby object, not just those that inherit from ActiveRecord
.
# app/models/payment.rb
monetize :amount
def self.monetize(field)
define_method field do
amount = send "#{field}_cents"
currency = send "#{field}_currency"
Money.new amount, currency
end
define_method "#{field}=" do |value|
raise ArgumentError unless value.respond_to? :to_money
money = value.to_money
send "#{field}_cents=", money.cents
send "#{field}_currency=", money.currency_as_string
money
end
end
This object should still pass all of our original tests with no issues.
How is This Any Different?!
I know, I know, this really didn’t do anything for us. We’re still defining the getters and setters on the class, but now we just have a new method that wraps it. But, we can now extract this method into its own Module and use it anywhere!
So, let’s start by pulling this block of code into its own file in our lib
directory.
# lib/monetizable.rb
module Monetizeable
def self.included(base)
base.send :extend, ClassMethods
end
module ClassMethods
def monetize(field)
define_method field do
amount = send "#{field}_cents"
currency = send "#{field}_currency"
Money.new amount, currency
end
define_method "#{field}=" do |value|
raise ArgumentError unless value.respond_to? :to_money
money = value.to_money
send "#{field}_cents=", money.cents
send "#{field}_currency=", money.currency_as_string
money
end
end
end
end
# app/models/payment.rb
class Payment < ActiveRecord::Base
include Monetizable
monetize :amount
end
Wrapping Up
We’ve now created a module that can be used in any class in our application that can “monetize” mutliple attributes. You could also hook into ActiveRecord
with an initializer to include the module automatically.
If you’re curious, I’ve also put this into a small gem. Be sure to check out the lint test to easily ensure that your classes are properly monetized.
This is officially the end of our series about handling money in Rails. I’ll continue to post on a regular basis about all of the things I’ve learned along my journey.