John Topley has been kind enough to submit his AssetsGraphed application to us for review. He even set up a publicly-accessible subversion repository for (a subset of) the code: http://johntopley.com/svn/projects/assetsgraphed/.
AssetsGraphed is, according to the site, “a simple asset tracking application that also tracks your data.� “Assets�, in this sense, is referring to financial assets, so you can keep track of your spending. Koz and I will be spending a few articles discussing better ways to approach some of the programming problems John has tackled in this application.
For this first article, I’d like to focus on how to refactor away some of the grosser kinds of code duplication. One of the first questions John asked us in his submission was about how to clean up the duplication in the “incoming� and “outgoing� controllers and models. (Compare them here: incoming and outgoing controllers; incoming and outgoing models.) As you can see, the two are almost completely word-for-word identical—a poster child for refactoring if there ever was one.
Let’s begin by looking at the models. In this case, the only textual difference between the two files is the name of the model:
1 2 3 4 5
$ diff app/models/incoming.rb app/models/outgoing.rb 1c1 < class Incoming < ActiveRecord::Base --- > class Outgoing < ActiveRecord::Base
Given that the two models have that level of similarity, my first suspicion was that they were really the same model, simply considered from different directions. What if, instead of Incoming and Outgoing models to track “money you are getting� and “money you are spending�, we simply defined a single “Item� model that encapsulated all transactions?
The differentiator between “incoming� and “outgoing� transactions then simply becomes the sign on the amount. “Incoming� transactions are positive numbers, and “outgoing� transactions are negative. As long as an index exists that includes the “base_amount� column, queries that ask for “base_amount < 0� and “base_amount >= 0� should be plenty efficient.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
class DefineItemsTable < ActiveRecord::Migration
def self.up
create_table :items do |t|
t.column "asset_id", :integer
t.column "category_id", :integer
t.column "transaction_date", :date
t.column "created_on", :datetime
t.column "base_amount", :integer
end
# for finding all items by asset
create_index :items, %w(asset_id base_amount)
# for finding all items by category
create_index :items, %w(category_id base_amount)
end
end
(That second index, for finding all items by category, may be superfluous, unless the application needs to retrieve items by category.)
Once that new table and model is in place, we can add some methods to the Asset class for returning the “incoming� and “outgoing� items:
1 2 3 4 5 6 7 8 9 10 11 12
class Asset < ActiveRecord::Base
# all items, incoming and outgoing
has_many :items, :dependent => :destroy_all do
def incoming
@incoming ||= find(:all, :conditions => "base_amount > 0")
end
def outgoing
@outgoing ||= find(:all, :conditions => "base_amount < 0")
end
end
end
The “do…end� syntax on the has_many association simply extends the association with an anonymous module containing the given methods. In other words, we can now do something like this:
1 2 3 4 5 6 7 8 9 10 11
# given some asset... asset = Asset.find(params[:id]) # we can return the list of _all_ items... asset.items # or only the incoming items... asset.items.incoming # or only the outgoing items... asset.items.outgoingKoz says:
An alternative to the anonymous modules is to simply declare two more associations.
1 2 3 4 5
class Asset < ActiveRecord::Base has_many :items, :dependent => :destroy_all has_many :incoming_items, :class_name => 'Item', :conditions => "base_amount > 0" has_many :outgoing_items, :class_name => 'Item', :conditions => "base_amount < 0" end
It’s a little simpler to understand, and clocks in at a whopping 6 fewer lines of code. The only difference is that instead of accessing the incoming items with @asset.items.incoming, you’d use @asset.incoming_items.
Once the models are unified, the task of unifying the duplicated controllers follows naturally, resulting in a single ItemsController. The client must simply submit the item amount as either positive (for incoming) or negative (for outgoing), and the categorization occurs automatically. If there is a concern about requiring users to enter negative numbers, you can pass an extra parameter to the create action that indicates whether the number should be negative or not.
I really love extreme refactorings like this—there is very little in programming that gives me as much pleasure as axing code.
No comments yet.
You must be logged in to add your own comment.