Refactoring When the Logic Starts to Grow
This post is for Rails developers learning object-oriented design.
A few weeks ago, someone I was mentoring was impressed by a presenter I’d written. I was really happy with the implementation—until the requirements changed and the code started to look gross.
I still wanted to keep the code easy to read. It was a fun refactor, and I figured it was worth sharing.
With the new requirements, we had to send different messages depending on how close a book was to its due date—sometimes urgent, sometimes just a friendly reminder, and a special case for books due today.
The presenter already felt like it was at its limit. Adding more conditions made it even harder to read, and it felt like the right time to clean things up.
The Original Presenter Class
class BookDueSoonPresenter
attr_reader :user, :book
def initialize(user:, book:)
@user = user
@book = book
end
def user_name
user.name
end
def subject
"Reminder: Book Due Soon"
end
def book_title
book.title
end
def due_date
book.due_date.strftime("%B %d, %Y")
end
def days_left
(book.due_date.to_date - Date.today).to_i
end
def return_message
messages.fetch(message_key) % { days: pluralized_days_left }
end
def message_key
days_left <= 2 ? :urgent : :friendly
end
def messages
{
urgent: "Please return it as soon as possible -- it's due in just %{days}!",
friendly: "You still have %{days} left."
}
end
def pluralized_days_left
"#{days_left} #{'day'.pluralize(days_left)}"
end
end
Extraction
A new class gives the behavior a name, a clear boundary, and a place to grow. All the logic for generating return messages now lives in the ExpiryMessages
class.
class ExpiryMessages
def initialize(book, time: Date.today)
@book = book
@time = time
end
def return_message
case days_left
when ..-1
overdue_message
when 0
due_today_message
when 1..2
urgent_message
else
friendly_message
end
end
def days_left
(@book.due_date.to_date - @time.to_date).to_i
end
def pluralized_days_left
"#{days_left.abs} #{'day'.pluralize(days_left.abs)}"
end
def overdue_message
"This book is overdue by #{pluralized_days_left}!"
end
def due_today_message
"This book is due today!"
end
def urgent_message
"Please return it as soon as possible — it's due in just #{pluralized_days_left}!"
end
def friendly_message
"You still have #{pluralized_days_left} left."
end
end
Smaller Presenter
The ExpiryMessages
class isn’t pretty — it’s full of conditionals and special cases. But that mess is now contained. The refactor led to a presenter that’s easier to scan and understand, and BookDueSoonPresenter
is once again a lightweight object that simply exposes data, the way presenters should in templates.
class BookDueSoonPresenter
attr_reader :user, :book
def initialize(user:, book:)
@user = user
@book = book
end
def user_name
user.name
end
def subject
"Reminder: Book Due Soon"
end
def book_title
book.title
end
def due_date
book.due_date.strftime("%B %d, %Y")
end
def return_message
ExpiryMessages.new(book).return_message
end
end
Giving the expiration message its own class made the presenter easier to read, the message logic easier to test, and — maybe most importantly — gave that group of conditions a name and a place to live. The same treatment can be applied to the subject
method, but going deeper felt like overkill for a blog post.