With experience, Rails developers gain super-power: they can translate business requirements into Rails features automatically. In this lesson we'll slow down and do the counter-wise: take a look at the decision-making process in slow-motion.
Let's say you were told to implement the following form:
As an experienced developer you come up with this code almost instantly:
class User < ApplicationRecord
validates_presence_of :username, :email, :address
validates_uniqueness_of :username, :email
validates_confirmation_of :email ⇠1️⃣
validates_acceptance_of :terms_of_service ⇠2️⃣
geocoded_by :address ⇠3️⃣
before_save :geocode ⇠3️⃣
end
Your boss, of course, impressed by the speed of development, brings the new set of requirements:
It takes a bit longer, but still, the changes are somewhat trivial:
class User < ApplicationRecord
DEFAULT_COLOR = "#fcaa84"
attr_accessor :moderation_mode ⇠1️⃣
validates_presence_of :username, :email, :address
validates_uniqueness_of :username, :email
validates_confirmation_of :email, on: :create ⇠3️⃣
validates_acceptance_of :terms_of_service, on: :create ⇠3️⃣
validates_presence_of :profession, :workplace if: :in_moderation_mode? ⇠2️⃣
geocoded_by :address
before_save :geocode, on: :create
before_validation :strip_and_downcase_username, on: :create ⇠4️⃣
before_validation :set_default_color_theme, on: :create ⇠4️⃣
def in_moderation_mode? ⇠2️⃣
!!moderation_mode
end
def strip_and_downcase_username ⇠4️⃣
self.username = username.strip.downcase
end
def set_default_color_theme ⇠4️⃣
self.color = DEFAULT_COLOR unless color
end
end
Aaand the boss is happy again! Of course, he needs another "little tweak" to the UI:
If it's checked, then during profile creation, we should create a default organization for that user, whatever it means.
The final code:
class User < ApplicationRecord
DEFAULT_COLOR = "#fcaa84"
attr_accessor :moderation_mode
attr_accessor :create_an_organization ⇠1️⃣
has_one :organization
validates_presence_of :username, :email, :address
validates_uniqueness_of :username, :email
validates_confirmation_of :email, on: :create
validates_acceptance_of :terms_of_service, on: :create
validates_presence_of :profession, :workplace, if: :in_moderation_mode?
before_validation :strip_and_downcase_username, on: :create
before_validation :set_default_color_theme, on: :create
after_save :create_default_organization, if: :should_create_organization? ⇠2️⃣
geocoded_by :address
before_save :geocode
def create_default_organization ⇠3️⃣
Organization.create(
address: address,
title: "#{username}'s organization", person: self
)
end
def in_moderation_mode?
!!moderation_mode
end
def should_create_organization?
!!create_an_organization
end
def strip_and_downcase_username
self.username = username.strip.downcase
end
def set_default_color_theme
self.color = DEFAULT_COLOR unless color
end
end
All these changes seem to be logical and Railswayish. Yet, when I used to write a code like this, I felt a certain degree of discomfort. Unfortunately, I was never able to explain to myself what is wrong with such code.
To make sure I am not the only one who feels that way, I posted this code snippet on Reddit and in few other places.
Not only I found people who felt the same, but I was also surprised by the variety of opinions I've got:
Not only the opinions are different, but recommendations on how to improve the code are very diverse as well.
You might be thinking now: "Oh, come on! He is going to teach us to use service objects and form objects, and how it fixes everything". Well, I used to be advocating for that a few years ago.
But it is not that simple. Splitting things into smaller pieces usually improves it to a certain degree. But if there is no strong reasoning behind this intention to split things, then it can bring even more complexity despite the original intention.
So, no. It is not about service objects, relax. It pretends to be a few levels deeper. I'll add more on this in the end.
For now, let's return to our dilemma.
Isn't it kind of strange that for so great framework and so mature community, we still haven't come to a consensus on the best way to implement such a simple piece of functionality?
Someone might say that it is fine to have such a diversity of opinions. Although, there's a trick. If we treat programming like engineering, then there should be not that many options.
Engineers thoroughly test multiple ways of doing a single thing, then select the optimal way, document it, and use for that specific thing in the future. Maybe optimize it a bit.
What we have now is the opposite of engineering - it is the Art. Even within the single community(Rails), there are dozens of ways of doing the same thing. Maybe the Ruby languages itself inspires for creativity, I don't know.
From the engineering perspective, it means that many of us are doing it in a very inefficient way!
We're going to fix it. Not all of it, but some things we will improve. We'll take a look at the code above from a number of different perspectives. This should help you develop an eye on small "violations" here and there. Rails tolerates them in the sake of "developers' happiness", but in the long run, the combined effect of these violations hits us so hard that happiness is no longer on our plate.
It becomes more about survival, desperation, and blaming. This is exactly the point where Rails disciples turn into Rails critics:
If you think that I'll be blaming Rails, you're wrong again. Well, I actually might a bit, but all in all, it is not about Rails. It is about the development of a proper reasoning model and understanding the true meaning of the code we write.
In the next lesson, we'll talk about specific Rails features, which mask the complexity of our code, so that we keep thinking that our code is fine when it is not. Next email should hit your inbox in a few days. Stay cool!
This was the first part of the Rails Pitfalls free mini-course. If you found it intriguing enough, consider sharing it with your co-workers - it is always better to discuss an important topic in a company, rather than dealing with it on your own.
For public sharing please use this link instead.