Just learn Rails (Part 2)
19 Jul 2015In a previous post I explained how “just learning Rails” is not as straight forward as the phrase portrays. Even the novice programmer who learns Ruby on Rails in a methodical progression may still run into hardship. However, this is not entirely the fault of young programmer, he or she was simply enabled into bad habits. Ruby on Rails enables the complete disregard of encapsulation. Notice how I did not say that Ruby on Rails prescribes less encapsulation, it simply enables it.
This idea, much like any idea posted to the Internet, is not new. Many intelligent people have come to this same realization. One such example is the Trailblazer framework, which aims to help inject some encapsulation and abstraction mechanisms Rails is lacking. (For those interested, a book has been written to more fully explain this framework).
Alright, so what am I really talking about? Ruby on Rails has certain features that make it easy to forget about encapsulation.
The Autoloader
A keyword that most Ruby on Rails developers rarely see on a daily basis is require
. A piece of Ruby on Rails magic called autoloading greatly decreased the frequency of the require
keyword in Ruby on Rails applications. This has many positive aspects for new and seasoned developers alike. But, it is not without drawbacks. One such drawback is that it makes ActiveRecord
objects ubiquitous. Whenever a developer has the urge to reach into the database, regardless of the context, they are able to do so.
This enables horrific code.
Imagine we have a view template that we want to display a user’s username and their pictures. Without any guidance, this following code could come into existence:
#
# Reminder, this is terrible code. Never do this.
#
<div>
<!--
Let's just go through all the users in the database.
This is a new application so we don't have that many users,
it will never run into scalability issues.
-->
<% User.all.each do |user| %>
<div class='username'>
<%= user.username %>
</div>
<div class='pictures'>
<!--
Make sure we have the only the picture url, just go ahead
directly to the database and let it handle
the downcasing that we, for some reason, need.
Also, Jim in Business Ops thinks it will be
super great to order the pictures randomly each time!
-->
<% Pictures.select('downcase(url)')
.where(user_id: user.id)
.order('random()').each do |url| %>
<img src="<%= url %>"/>
<% end %>
</div>
<% end %>
</div>
#
# Never replicate the above code or you will be very sad
#
Notice how deep in the presentation layer (the view) we have direct database calls. This completely violates the encapsulations that an MVC framework should provide. Since all the models exist everywhere, nothing stops a programmer from writing it in this way. The code above should make every ruby programmer cringe.
Separating concerns and responsibilities helps prevent repetition and boosts comprehension. The above code should be a composition of multiple objects with entirely different responsibilities. That concept is not always intuitive for some developers, especially those just starting out. One might ask: “Why would I make a whole mess of objects to do the same thing that these 14 lines can do?”. The simple answer is because these problems are not new. Ruby on Rails did not expose these abstraction and encapsulation issues for the first time in the history of software engineering. They have been around for a very long time. Smarter software artisans than I saw these problems and created guidelines to solve them. Following their lead will save all of us in the long run.
So what can we do about our misguided code above? Let us try moving some responsibilities around and see how it changes.
Add a dash of encapsulation
# The user class just needs to know about the database connection.
# ActiveRecord takes care of that detail for us, adding a scope
# is a supplementary addition to that logic.
class User < ActiveRecord::Base
scope :with_pictures, ->{ includes(:pictures) }
end
# We can create a PORO (Plain Old Ruby Object) to house the user
# object and extract, downcase, and randomize its picture urls.
class UserPresenter
def initialize(user)
@user = user
@pictures = user.pictures
end
def username
@user.username
end
def picture_urls
@pictures.map do |photo|
photo.url.downcase
end.shuffle
end
end
# The controller handles linking up each User with a UserPresenter
# then saves the list to a variable to be used by the view.
class UsersController < ActionController
def show
@user_presenters = []
User.all.with_pictures.each do |user|
@user_presenters << UserPresenter.new(user)
end
end
end
Finally, the view logic only knows about a presenter for each user. From the views perspective, the presenter object only has two methods: username and picture_urls. It does not need to know about database structure, schema, syntax or business logic. It has one job, to display data:
<div>
<% @user_presenters.each do |presenter| %>
<div class='username'>
<%= presenter.username %>
</div>
<div class='pictures'>
<% presenter.picture_urls.each do |url| %>
<img src="<%= url %>"/>
<% end %>
</div>
<% end %>
</div>
Conclusion
The code that was rewritten is far from perfect. However, it is a noticeable improvement over its original version. I am in no way claiming to be the creator of these design patters, simply a spokesperson for good encapsulation and object oriented design. When respected and correctly utilized, Ruby on Rails is a fantastic tool for web development. When learning Ruby on Rails, it is imperative to stand by software engineering paradigms. Just because a framework allows it, doesn’t mean that it is necessarily a good thing to do.