This is the third in a series of three posts that will demonstrate implementation of three common Rails design patterns: the adapter pattern; service objects; the decorator pattern
You can find the code for these posts here You can find a demo of the app, deployed to Heroku, here.
In the previous two posts in this series, we made our controllers clean and DRY by implementing the adapter patter, along with a series of service objects.
We built adapters to wrap our interactions with the GitHub and Twilio APIs and we built services to carry out the tasks of creating new repos, updating issues and sending email and text notifications.
In this final part of our refactoring series, we'll clean up our views and models by implementing the decorator pattern.
What is the Decorator Pattern?
The decorator pattern allows us to add behavior to an instance without affecting the behavior of all the instances of that class. We can “decorate” or add behavior to, a model instance before passing it to a view. Or, we can decorate an instance within a view.
By extracting complex presentation logic out of our views and models, and into decorator classes, we can simplify views (which should be stupid) and models (which shouldn't know about presentation), and decorate instances as needed.
Let's get started!
Implementing the Decorator Pattern
View Logic
Let's take a look at one of our view-related code smells––the code we use to display a repository's name:
# app/views/repositories/_repo.html.erb
<%= @repository.name.gsub("-", " ").titleize %>
The same code is used on the issue's show page, when rendering the name of the repo to which it belongs:
# app/views/issues/show.html.erb
<%= @issue.repository.name.gsub("-", " ").titleize %>
Code Smells
Not only is this code repetitious, it's also using an awful lot of Ruby to manipulate a string into looking pretty for us. This is not the responsibility of the view. The view just needs to sit there and look pretty, like a pre-Suffragism Victorian lady.
Heaven forbid I handle my own view logic. I prefer to just sit here and mind my cats. - pintrest.com
Let's implement a decorator to handle the presentation of a repository's name.
Fix It
- Define a subdirectory,
app/decorators
- Define a file
app/decorators/repository_decorator.rb
Here we'll define our decorator with a method, #display_name
, to encapsulate the repo name display logic.
# app/decorators/repository_decorator.rb
class RepositoryDecorator < SimpleDelegator
def display_name
name.gsub("-", " ").titleize
end
end
Note that our decorator inherits from SimpleDelegator
.
What is SimpleDelegator?
SimpleDelegator
is a native Ruby class that
provides the means to delegate all supported method calls to the object passed into the constructor. –– Ruby Docs
In other words, we can instantiate our RepositoryDelegator
with an argument of a Repository
instance, and, because it inherits from SimpleDelegator
, we can treat that new delegator instance just like an instance of Repository
, with the added functionality of our delegator.
Let's use it!
# app/views/repositories/_repo.html.erb
<%= RepositoryDecorator.new(@repository).display_name %>
# app/views/issues/show.html.erb
<%= RepositoryDecorator.new(@issue.repository).display_name %>
Let's take a look at another code smell that could benefit from some decorator refactoring, this time in the model.
Model Methods
Currently, we have a model method on our User
model that deals with presentation logic.
# app/models/user.rb
def display_phone_number
phone_number && !phone_number.empty? ?
phone_number : "
<i>add your phone number to receive text
message updates</i>".html_safe
end
This method is called in the view like so:
# app/views/users/show.html.erb
...
<p>phone number: <%= @user.display_phone_number %>
Code Smell
It is absolutely a violation of the separation of concerns and the single responsibility principle to have the User
model be responsible for this presentation logic.
Fix It
Let's build a decorator!
- Generate a file,
app/decorators/user_decorator.rb
Here, we'll define our decorate to have a method, #display_phone_number
.
# app/decorators/user_decorator.rb
class UserDecorator < SimpleDelegator
def display_phone_number
phone_number && !phone_number.empty? ?
phone_number : "
add your phone number to receive text
message updates".html_safe
end
end
Now, we can use it in our views like so:
# app/views/users/show.html.erb
...
<p>phone number: <%= UserDecorator.new(@user).display_phone_number %>
Much cleaner!
Conclusion
That's it for our Rails Refactoring series. Keep in mind that these posts are not meant to say "this is how you refactor", but rather "here are some techniques you can apply when refactoring".
You can find Part I here; and Part II here. You can find the final refactored code for this project here, and the pre-refactor version here.
Lastly, here are some resources:
- Uncle Bob, Does Organization Matter?
- Uncle Bob, Code Hoarders
- Sandi Metz, Practical Object-Oriented Design in Ruby
Happy coding!