Problem
I’m trying to figure out how to refactor some code based on what codeclimate is telling me. So far it seems that any method that is longer than 5 lines or has an if
statement is “complex”. In this instance, I’m trying to create an event for a calendar based off of a meeting record.
Here is the Event
model:
class Event < ActiveRecord::Base
validates :title, :eventcolor_id, presence: true
belongs_to :meeting
belongs_to :eventcolor
def self.create_from_meeting(meeting, eventcolor)
meeting.meeting_date_end ||= meeting.meeting_date_start
Event.find_or_initialize_by(meeting_id: meeting.id) do |e|
e.title = meeting.title
e.datetime_start = Time.zone.local_to_utc(meeting.meeting_date_start)
e.datetime_end = Time.zone.local_to_utc(meeting.meeting_date_end)
e.location = meeting.location
e.address1 = meeting.address1
e.address2 = meeting.address2
e.city = meeting.city
e.state = meeting.state
e.zip = meeting.zip
e.description = (!meeting.description.blank?) ? meeting.description : ''
e.eventcolor_id = eventcolor
e.save
e.committees << meeting.committee if !meeting.committee.nil?
end
end
end
which is called from my meetings_controller
with this line:
Event.create_from_meeting(meeting, params[:eventcolors_select])
Solution
A few things caught my eye:
-
You don’t check whether
e.save
actually worked. If it fails, you’ll have problems down the line. -
meeting.meeting_date_end ||= meeting.meeting_date_start
Don’t set an attribute on a record you don’t “own”! In this case, a meeting object is being passed to the method to be copied – not altered. Use a local variable instead. It could be a nasty surprise to other parts of the code, to find that the meeting’s been altered. Treat the meeting as strictly read-only. -
Why are some of your
meeting
‘s date/time-attributes prefixed with the class name? It’s redundant; they’re attributes on aMeeting
instance, so there’s no reason to name themmeeting_*
. You also prefixevent.datetime_start
with the type, which isn’t Ruby-like or Rails-like either. In both cases, something likestarts_at
andends_at
would be more than enough. It’d follow the convention set byupdated_at
andcreated_at
and be immediately recognizable. -
Speaking of, why are your meeting’s date/time attributes defined in the local timezone? I’d advice you to store everything as UTC, so you don’t have to worry about conversion (as you do in your current code). Because timezone conversion always ends up being painful.
-
e.description = (!meeting.description.blank?) ? meeting.description : ''
The parentheses aren’t necessary, but moreover there’s thepresent?
method, which does what you want here: The opposite ofblank?
.
However, you could also just doe.description = meeting.description || ''
sinceblank?
triggers onnil
s and empty strings, but the latter is the default anyway, so you really only want to default in case ofnil
. -
Similarly, this
if !meeting.committee.nil?
can be written a few other ways:unless meeting.commitee.nil? if meeting.commitee.present? if meeting.commitee
The last one is the simplest and clearest, but all of them are, to my eyes, better than saying “if the committee is not not there“
-
My biggest concern is that this is a lot of data duplication – seemingly for no reason. Since
Event
already belongs to aMeeting
, why duplicate anything? Everything should be available since you haveevent.meeting
. Duplicating everything doesn’t really make sense as long as you have theMeeting
record. All you gain is the task of keeping two separate sets of data in sync whenever one changes. To me, this approach doesn’t really make sense.
So I’d stick with:
event = meeting.events.create(eventcolor_id: params[:eventcolors_select])
and maybe add some accessor methods (decorators) to Event
to give you the correct datetime_start
and datetime_end
. No need for the method at all.
Semantically, I’d prefer to have it flipped the other way, though: Make a meeting belong_to
an event. A calendar event is a pretty generic entity: It can be anything with a time and (optionally) a place. A meeting, meanwhile, is a more specific thing. So to my mind, it’d make more sense to have a meeting attached to a calendar event, rather than the other way around.
Or you can do Single Table Inheritance with Event
as the parent class. Or have an even more generic model – or models – the wrap the time and/or place, and have both Event
and Meeting
refer to that.
Basically, there are many ways to handle this without duplicating data.
Meanwhile, just looking at the code as-is, you can get rid of some lines:
# Note: May raise ActiveRecord::RecordInvalid
def self.create_from_meeting(meeting, eventcolor)
Event.find_or_initialize_by(meeting_id: meeting.id) do |e|
# copy all the identically-named attributes
%w(title location address1 address2 city state zip description).each do |attribute|
e.send("#{attribute}=", meeting.send(attribute))
end
e.datetime_start = Time.zone.local_to_utc(meeting.meeting_date_start)
e.datetime_end = Time.zone.local_to_utc(meeting.meeting_date_end || meeting.meeting_date_start)
e.eventcolor_id = eventcolor
e.save! # raises an exception if it fails
e.committees << meeting.committee if meeting.committee
end
end
But it’s still not very pretty, just shorter. Again, the main issue is the duplication.