Finding and automating repeated ideas in your code can be a great way to avoid errors and make yourself feel at home. As you build your own software, you and your team can create a framework that makes implementing new features a breeze. Here's how I implemented some changes that helped us do some serious cleanup.
You may recall from my last post about displaying bad data that my presenter looked a bit like this:
require 'delegate'
class UserPresenter < SimpleDelegator
alias_method :user, :__getobj__
def initialize(object, view)
super(object)
@view = view
end
end
That leaves out some a custom method, but you get the picture of how it's created. This wasn't a new gem I pulled in to solve a problem, it was built from scratch.
Once I have something that works and feels comfortable, I end up finding a need for it in different ways. Sure enough, I found that I could use presenters in other places. Classes like ProfilePresenter
, ShoppingCartPresenter
, and others began to appear.
There are a few things I did to make myself feel at home with them and, better yet, make it easier for others to implement their own.
Let Ruby do the work
I don't want to be recreating this same bit of code when I find a new need for a presenter.
One helpful aspect of my user presenter was the user method I had available. It was a name that expressed the object much better than the standard __getobj__
method. If I needed similar methods in other presenters, I'd rather not be writing alias_method
over and over again.
The ProfilePresenter
should have a profile
method and the ShoppingCartPresenter
should have a shopping_cart
method, and so on.
So I created a starting point that would do this for me.
I decided that I would stick to a simple naming convention using the class name of my presenter to generate the method accessors.
require 'delegate'
class Presenter < SimpleDelegator
def self.inherited(klass)
# get the part of the class name we want like "ShoppingCart"
base_name = klass.name.split('::').last.sub('Presenter','')
# downcase and underscore (without activesupport) like "shopping_cart"
wrapped_object_name = base_name.gsub(/([a-z][A-Z])/){ $1.scan(/./).join('_') }.downcase
klass.send(:alias_method, wrapped_object_name, :__getobj__)
klass.send(:alias_method, "#{wrapped_object_name}=", :__setobj__)
end
end
When any class inherit's from that, it'll automatically alias methods based upon the name of the class. It's equivalent to:
class ShoppingCartPresenter < SimpleDelegator
alias_method :shopping_cart, :__getobj__
alias_method :shopping_cart=, :__setobj__
# more custom methods that we need
end
Instead of all that, however, it just looks like this:
class ShoppingCartPresenter < Presenter
# more custom methods that we need
end
Displaying potentially dangerous data
My UserPresenter
had become useful when we needed to display a bio for a user. Our data could contain HTML and a we needed to be careful about what would be displayed. We certainly didn't want arbitrary scripts being loaded from any bio content.
Given that I had a Rails application, we were able to use the standard sanitize method to clean up HTML content. And now that we've got presenters, it gave us a perfect place to tie our objects and views together so that our template files could be simplified.
Originally, our view looked like this
<%= sanitize @user.bio %>
But we had already made some changes to use a @presenter
in another place, so we can lean on that to make our view template simpler.
Our presenter initializes with combination of an object and view, so we can handle this behavior inside:
class UserPresenter < Presenter
def bio_html
@view.sanitize(user.bio)
end
end
Then our view template becomes a bit more predictable when moving over to the @presenter
:
<%= @presenter.bio_html %>
If we want to provide this ability to all presenters, we can add the behavior to our base class:
class Presenter < SimpleDelegator
def sanitize(*args)
@view.sanitize(*args)
end
end
class UserPresenter < Presenter
def bio_html
sanitize(user.bio)
end
end
A big benefit there is that we had better control over what created our sanitized html. Our code managed the dependency on the Rails sanitization behavior and if, for any reason, we decided to use an alternate strategy for sanitizing our data, we'd have a single place to look to make the change.
Simplified interfaces
Creating our own conventions made using our code predictable and easy. It gave us sensible expectations for how it would work.
Moving the responsibility for creating a sanitized bit of data from the view template into a presenter object helped us to better manage our templates with simpler code. And it helped us stick to a single interface for data display.
Next time I'll cover how I was able to use wrappers while looping over collections of objects and how we removed if/else
statements from our views.
How have you used your own conventions to simplify your process?