Ruby on Rails is all about abstraction. It is important to make the code reusable and hold it all together. On the other hand too much abstraction might hamper the software and make code hard to understand. Today we will talk about the right balance between abstraction and special cases in Ruby on Rails development and learn how to use them correctly.
For or Each?
Newbies in Ruby on Rails are often puzzled with what should they use: for or each.
For is a special case and each is an abstraction. Let’s see how they look like:
For:
[ruby]
for student in students
puts student.address
end
[/ruby]
Each:
[ruby]
students.each do |student|
puts student.address
end
[/ruby]
For is not so commonly used in Ruby and there’s a reason for that: it creates a single concept and will work only for a single case. At the same time each is much easier to repurpose, it is widely used in so-called method and blocks approach used in Ruby on Rails. And the best thing about each is that one can build it himself in Ruby:
[ruby]
class Array
def each(&block)
unless empty?
first, *rest = self
block.call(first)
rest.each(&block)
end
end
end
[/ruby]
In the block above a number of basic methods are used, but we are sure pretty soon you will get totally familiar and comfortable with them.
At the same time you should keep in mind that for, being a special syntax, is not built on anything, so one can’t implement it himself.
Being able to implement idea using what you already got significantly reduces the amount of abstractions required, so we highly recommend this approach.
Nil or Array?
Another commonly used conception is nil. It is used in cases when there can be or can be not a value. For example there can be a field address, which some students have filled in and some have not. For those, who haven’t, nil is returned.
Let’s see how it works.
[ruby]
Student.new(address: “Green street 12”).address
=> “Green street 12”
Student.new(address: nil). address
=> nil
[/ruby]
Though looking pretty good, nil implies that we have to verify existence of the address first:
[ruby]
students.each do |student|
unless student. address.nil?
StudentMailer.update(student. address, update).deliver_now!
end
end
[/ruby]
Is this abstraction really necessary? We think that in such cases Array can be more efficient.
[ruby]
Student.new(addresses: [“Green street 12”]).address
=> [“Green street 12”]
Student.new(address: []).address
=> []
students.each do |student|
student.addresses.each do | address |
StudentMailer.update(student.address, update).deliver_now!
end
end
[/ruby]
While nil says that there be or not be something, Array is saying that there’s unknown number of something, which is a much more generic solution. Nil can be only 1 or 0, whereas Array can be any number. No wonder we recommend avoiding abstract nil in such cases.
Conditionals or Null Object?
Every time you use conditionals, you are basically creating a special case. Common abstractions can easily replace them:
Conditional:
[ruby]
students.each do |student|
unless student.address.nil?
StudentMailer.update(student.address, update).deliver_now!
end
end
[/ruby]
Abstraction:
[ruby]
students.map(&:address).compact.each do |address|
StudentMailer.update(address, update).deliver_now!
end
[/ruby]
Null Object abstraction can also help you get rid of a bunch of special cases.
[ruby]
class Guest
def admin?
false
end
def can_edit?(post)
false
end
def name
“Guest”
end
end
[/ruby]
This calls lets you not bother whether the student is signed in. In another place you can repurpose it to define guests behavior.
How to use reduce abstraction?
If you want your code to be even easier to understand, we recommend to perform folding with the help of reduce abstraction. Let’s compare special case and abstraction.
Special cases:
[ruby]
class Game
def score
result = 0
rounds.each { |round| result += round.score }
result
end
def competitors
result = []
rounds.each do |round|
round.students.each do |student|
result << student.address
end
end
result
end
end
[/ruby]
Abstraction:
[ruby]
class Game
def score
rounds.reduce(0) { |result, round| result + round.score }
end
def competitors
rounds.reduce([]) { |result, round| result + round.students.map(&:address) }
end
end
[/ruby]
Here one can add additional level of abstraction:
[ruby]
class Game
def score
rounds.sum(&:score)
end
def competitors
rounds.map(&:students).flat_map(&:address)
end
end
[/ruby]
Sum and flat_map would allow providing a specialization for a fold.
As you see there’s no answer to what you should use: special cases or abstraction. The clue to success lies in keeping it balanced and providing the most unified solution possible.
Questions? Comments? Let’s talk about them in the comments section below.