Do not apply the “Clean Code” to your Ruby project
In 2008, Robert Martin published his book “Clean Code,” where he teaches a series of what he considered to be best practices for Java developers. Unfortunately, despite many of the techniques in the book being perfectly suitable for the Ruby language, many others are not; because Java had (and still has) a series of limitations that do not exist in Ruby.
To illustrate how the “Clean Code” can be inappropriate for Ruby, I will start with this example, an algorithm that calculates the square root of a number:
# code 01
def sqrt(x)
guess = 1.0
loop do
if (guess**2 - x).abs < 0.001
return guess
else
guess = (guess + (x / guess))/2
end
end
end
In chapter 3, Robert Martin explains how to write more legible functions. A developer that follows the teachings from his book would likely rewrite the code in this way:
# code 02
def average(x, y) = (x + y)/2
class Sqrt
def initialize(x) = @x = x
def call() = sqrt_iter(1.0)
private
def good_enough?(guess) = (guess**2 - @x).abs < 0.001
def improve(guess) = average(guess, (@x / guess))
def sqrt_iter(guess)
if good_enough?(guess)
return guess
else
return sqrt_iter(improve(guess))
end
end
end
The two previous codes are implementations of the same algorithm that I took from the book “Structure and Interpretation of Computer Programs,” also known as “Purble Book,” written by Harold Abelson and Gerald Jay Sussman with Julie Sussman. The authors use a similar style to Robert Martin; they subdivide more extensive procedures into smaller ones and name the more minor functionalities to make the source more legible. This is how the algorithm is implemented in the “Purple Book”:
;; code 03
(define (average x y)
(/ (+ x y) 2))
(define (square x)
(* x x))
(define (sqrt x)
(sqrt-iter 1.0 x))
(define (sqrt-iter guess x)
(if (good-enough? guess x)
guess
(sqrt-iter (improve guess x) x)))
(define (good-enough? guess x)
(< (abs (- (square guess) x)) 0.001))
(define (improve guess x)
(average guess (/ x guess)))
After giving this example, the author explains why this code is problematic:
The problem with this program is that the only procedure that is important to users of
sqrt
issqrt
. The other procedures (sqrt-iter
,good-enough?
, andimprove
) only clutter up their minds. They may not define any other procedure calledgood-enough?
as part of another program to work together with the square-root program, becausesqrt
needs it. The problem is especially severe in the construction of large systems by many separate programmers. For example, in the construction of a large library of numerical procedures, many numerical functions are computed as successive approximations and thus might have procedures namedgood-enough?
andimprove
as auxiliary procedures. We would like to localize the subprocedures, hiding them insidesqrt
so thatsqrt
could coexist with other successive approximations, each having its own privategood-enough?
procedure. To make this possible, we allow a procedure to have internal definitions that are local to that procedure. For example, in the square-root problem we can write”
;; code 04
(define (average x y)
(/ (+ x y) 2))
(define (square x)
(* x x))
(define (sqrt x)
(define (good-enough? guess)
(< (abs (- (square guess) x)) 0.001))
(define (improve guess)
(average guess (/ x guess)))
(define (sqrt-iter guess)
(if (good-enough? guess)
guess
(sqrt-iter (improve guess))))
(sqrt-iter 1.0))
We must apply the same logic from code 04 to 02; a code calling the sqrt
should not be concerned about its internal workings.
Nevertheless, a developer using the callable object in code 02 must know it is called Sqrt.new(x).call()
instead of sqrt(x)
because of how it is implemented.
When Robert Martin was teaching about small functions, he was not creating something new; he was adopting the same technique that we can see in Scheme.
But Java did not have nested methods or similar things, such as lambdas and procs, leading him to use callable objects as substitutes.
However, Ruby has lambdas, allowing us to translate the code 04 without any adaptation; the workaround taught by Robert Martin is unnecessary in Ruby; we can write our method without turning it into a class and still making it easier to read:
# code 05
def average(x, y) = (x + y)/2
def sqrt(x)
good_enough = ->(guess) {(guess**2 - x).abs < 0.001}
improve = ->(guess) {average(guess, (x / guess))}
sqrt_iter = lambda do |guess|
if good_enough.call(guess)
return guess
else
return sqrt_iter.call(improve.call(guess))
end
end
sqrt_iter.call(1.0)
end
It is important to note that Scheme has tail call optimization by design; using a loop instead of a recursive iterator can be more appropriate in Ruby. Also, it is unnecessary to use lambdas to name some equations, as the original code in Scheme; it is possible to achieve the same result using variables. Finally, I do not believe that size is a good measurement for the legibility or simplicity of a method. Applying these two changes can result in a code like this that I also consider very legible:
# code 06
def average(x, y) = (x + y)/2
def sqrt(x)
guess = 1.0
loop do
good_enough = (guess**2 - x).abs < 0.001
if good_enough
return guess
else
improve = average(guess, (x / guess))
guess = improve
end
end
end
Using callable objects instead of nested functions is not something you can do without collateral effects; you needlessly increase the number of modules in your system. Nonetheless, Robert Martin sees no problem in having several classes in your system, as he stated:
However, a system with many small classes has no more moving parts than a system with a few large classes. There is just as much to learn in the system with a few large classes. So the question is: Do you want your tools organized into toolboxes with many small drawers each containing well-defined and well-labeled components? Or do you want a few drawers that you just toss everything into?”
But his arguments are fallacious; you have at least more names to learn when you have more classes. For example, if I could say: “screwdriver” for a screwdriver to appear right in my front, it would be irrelevant where it was before I summoned it. Therefore, having to call “screwdriver from drawer X” would only make my life harder. So, if you want to make your code clean, you should make it clean not only by how you implement your methods but by creating simple interfaces for them.
It is imperative to understand the history and the reasoning behind a technic before adopting it. It is also essential to know the difference between languages and their internal functioning when adopting a technic from one language to another. Otherwise, you are likely to make your code unnecessarily complex. Therefore, applying to your Ruby code adaptations made from Scheme to Java is pointless as the adjustments are unnecessary in Ruby. If you want to borrow techniques from other languages to use in Ruby, I recommend borrowing them from Lisp (and its variants) and Smalltalk, as these languages are very similar to Ruby in their philosophies.