Replacing An If Statement With An Object


I found myself writing this code yesterday:

def to_s
  if status == AssessmentStatus::Open
    date = get_assessment_short_date(:date_opened)
    "#{assessment.name}, Opened #{date}"
  else
    date = get_assessment_short_date(:date_closed)
    "#{assessment.name}, Closed #{date}"
  end
end

I wrote it and moved on. It worked and it did what I needed it to do – format a string to show when an assessment was opened, or when it was closed, depending on the current state of the assessment. A few moments later, I found myself writing the same code in my LabOrder class. I needed to know the date of the order vs the date it was received, depending on the state of the object.

When I saw myself writing this for the second time, I realized I was missing an abstraction. I needed to stop writing procedural code and start writing objects instead.

Not Quite A State Machine

Remembering this little quote and re-examining the code I was writing, I thought about creating a simple state machine in my app. I would only have a few transitions – from open to closed, or from ordered to received – but it seemed like a nice clean way to handle things. After doing a bit of looking around at the various state machine implementations for ruby, though, I decided I didn’t need that much complexity in my app. I already have the business logic of transitioning from Open to Closed, for my assessment, wrapped up in my Assessment object. It would be easier to just move my object’s `status` attribute into a class that contained the actual status and the date that the status occurred. (Note that I also renamed the field to `state` instead of `status`, on the Assessment.)

Here’s what that class looks like:

class AssessmentStatus
  include Mongoid::Document

  Open = "open"
  Closed = "closed"

  embedded_in :assessment, :inverse_of => :state
  
  field :status, :default => Open
  field :date, :type => DateTime, :default => DateTime.now
end

It’s a very simple class with no real logic in it at this point. However, it gives me a starting point to move forward in the event that I do find myself needing a complete state machine. For now, though, having the status and date stored in it’s own class allows me to change my `.to_s` method on the Assessment class:

def to_s
  "#{assessment.name} #{state.status} #{state.date.short_date}"
end

(Note the `.short_date` method on my date field is a helper method that I created in a DateTimeFormats module, which gives me several date and time formats that I use throughout my app)

“An Object Is A Choice”

I don’t remember who said this originally, but it’s been quoted on twitter recently and it has stuck with me for a few weeks now. When I first read this statement, I misunderstood the context. I thought it was saying you can choose to create an object, or not; that there is no hard and fast rule that says everything must be an object. While this interpretation may hold some ground on, too, I think the real intention of this statement was to say that you can write better code by removing flow-control logic (if statements, switch statements, etc) and replace them with objects. Stated a little differently, an object can be used in place of flow-control logic.

That’s not to say you should never use an if statement, though. There are times when you don’t necessarily need an object; when the if-statement or switch statement is sufficient for your design. Remember, though, that there are trade-offs that need to be accounted for. A good guideline to know when you should have an object instead of an if or switch statement, is when you see the same if or switch statement in more than one place. Where two or more ifs are repeated, an object should be present.

Getting Started With Thor