What is 'better' code?
June 27, 2012 📬 Get My Weekly Newsletter ☞
We all want better code. Rails creator David Heinemeier Hansson said that the only way to evaluate a code change is if the new code is “better” than the old. Of course, he didn’t define what he meant by “better”. At Scottish Ruby Conf, Dave Thomas said that good code is code that is easy to change. This is a bit more specific, but not really enough to give any real direction.
Let’s see if we can derive a real understanding of code quality.
What do we do with code? In decreasing order of frequency:
- We execute it. Hopefully, our code spends most of its time executing.
- We read it. To gain an understanding, to review it, to change it, we must read it.
- We change it. To add new features, or fix bugs, we must change it.
- We write it. On occasion, we’ll write new code.
The most frequent thing we do with code? Execute it. Code that runs, based on our current understanding (i.e. passes its tests), is the absolute minimum of acceptability. It’s at this point that average developers typically stop. If it works, ship it!
We aren’t average developers. We want to do better.
With almost equally great frequency, our code gets read by humans. We might read code to prepare for a change. We might read code to understand how a business rule works. We might read code as an example of how to do something, or to gain an understanding of some abstract concept. We read code a lot.
Is there anything objective we can say about code readability?
Readability
Readability answers the question “How quickly can someone understand this code?” We must first define “someone”. A good rule of thumb is “any developer that could be hired to work here”. You may need to get more specific, but generally constraining the context to your current team will work well.
Once we’ve got context for understanding code, the most obvious thing we could measure would be its size.
Size
Size can mean two things: length (the number of lines of code) and density (the amount of information per line of code). The more code you must evaluate, by either measure, the longer it will take to come to an understanding. The distinction between length and density is interesting. Short, but dense code, can be just as difficult to grasp as long sparse code.
def create_new_person(first_name, last_name, birthdate)
raise "first name and last name required" if first_name.nil? || last_name.nil?
Person.create(:first_name => first_name, :last_name => last_name).tap do { |person|
person.age = Time.now.year - birthdate.year if birthdate.present?
}
end
Here’s the same routine, rewritten to be as sparse as possible:
def create_new_person(first_name, last_name, birthdate)
if first_name.nil? || last_name.nil?
raise "first name and last name required"
end
person = Person.create(:first_name => first_name, :last_name => last_name)
if birthdate.present?
person.age = Time.now.year - birthdate.year
end
person
end
Which one do you find easier to understand? I would argue that the answer is not so clear-cut. What is interesting is that modern languages, like Ruby or Scala, tend to encourage denser, shorter programs. Some densly-packed statements are idiomatic, and are easily understood, while others become impeneatrable code golf. Know the difference and you can get a good sense of the size of a piece of code.
Variables
Any field, parameter, global, or local variable is a “variable” for the purposes of code readability. Variables are placeholders for the calculations our code performs, and the more of them there are, the more abstract pieces of data you must hold in your head in order to understand a piece of code.
Beyond the raw count of variables, the scope of each variable can also affect our understanding of code. A routine that uses nothing but local variables will be easier to undestand than one using all globals. Since globals can change outside of the routine you are reading, you need to have a higher level grasp of the system, so you can understand what possible values those variables might have. The smaller the scope of a variable, the easier it is to understand what values it might have, and the easier it is to understand the code it’s used in.
Of course, variable names are important, too. Descriptive (and accurate) names help our understanding, while symbol or inaccurate names can harm it.
Here’s a pattern I’ve seen in complex controllers, where ivars are used to pass variables between methods (you’ll need to imagine many other controller methods here):
class PeopleController < ApplicationController
def destroy
id = params[:id]
@person = Person.find(id)
if can_destroy?
@person.destroy
redirect_to persons_path
else
flash[:error] = @error
redirect_to persons_path
end
end
private
def can_destroy?
if @person.admin?
@error = 'You cannot delete an admin'
false
elsif @person.orders.unfulfilled.any?
@error = 'Person has unfullied orders'
false
else
true
end
end
end
Notice how both outcomes of destroy
are redirects, yet we are setting @person
. In a Rails controller, you create ivars to communicate data to the view, but for a redirect, these variables don’t apply. @person
is effectively a parameter passed to can_destroy?
but without declaring it as a parameter. Further, @error
is being initialized in can_destroy?
and acts as a return value. Finally, does id
need to be a variable at all? It’s only used in one place. Here’s a version that keeps variables to a minimum scope:
class PeopleController < ApplicationController
def destroy
person = Person.find(params[:id])
error = can_destroy?(person)
if error.nil?
person.destroy
redirect_to persons_path
else
flash[:error] = error
redirect_to persons_path
end
end
private
def can_destroy?(person)
if person.admin?
'You cannot delete an admin'
elsif person.orders.unfulfilled.any?
'Person has unfullied orders'
else
nil
end
end
end
Not only is the code a bit shorter, but each routine is simpler to understand, because the scope of the variables used are constrainted to only where they are needed.
Number of classes/methods
This is where things get interesting. If you need to follow the path of execution through many methods or classes to get an understanding of some code, it’s going to be harder to do so. Of course, with fewer classes, you’ll tend toward larger methods which, of course, can also be hard to understand.
Consider the refactor from my controversial blog post. In that post, I extracted a class from a Rails controller to handle the business process of creating a new user. Although the two classes were both very short and easy to understand, the entire codebase went from one class that contained all the code, to two classes. Which is easier to understand? It depends. But, by trying to quantify the differences in the code, we can approach an understanding.
Paths through the code
Often referred to as “complexity” in computer science, the number of possble paths of execution through a piece of code can greatly affect its ability to be understood by a person. Consider this slightly modified version of can_destroy?
from our example above:
def can_destroy?(person)
errors = []
if person.admin?
errors << 'You cannot delete an admin'
end
if person.orders.unfulfilled.any?
errors << 'Person has unfullied orders'
end
return errors.join(',')
end
There are two if
statements here, which gives us four possible ways through this code. This means that, to gain a real understanding of this code, we need to mentally play through all four scenarios in our heads. Since the expression of each if
statement is simple, this isn’t so bad. What if we needed to add a feature where employees are also not allowed to be destroyed in our controller?
def can_destroy?(person)
errors = []
if person.admin? || person.is_employee?
errors << 'You cannot delete an admin or employee'
end
if person.orders.unfulfilled.any?
errors << 'Person has unfullied orders'
end
return errors.join(',')
end
We’ve added an additional case for our first if
statement, and so we have more paths:
person.admin?
true,#is_employee?
false,orders.unfulfilled.any?
falseperson.admin?
true,#is_employee?
true,orders.unfulfilled.any?
falseperson.admin?
true,#is_employee?
false,orders.unfulfilled.any?
trueperson.admin?
true,#is_employee?
true,orders.unfulfilled.any?
trueperson.admin?
false,#is_employee?
false,orders.unfulfilled.any?
falseperson.admin?
false,#is_employee?
true,orders.unfulfilled.any?
falseperson.admin?
false,#is_employee?
false,orders.unfulfilled.any?
trueperson.admin?
false,#is_employee?
true,orders.unfulfilled.any?
true
If we were to extract the first if
statement’s expression to a method, we’d reduce the complexity of this code:
class Person
def deletable?
person.admin? || person.is_employee?
end
end
def can_destroy?(person)
errors = []
if person.deletable?
errors << 'You cannot delete an admin or employee'
end
if person.orders.unfulfilled.any?
errors << 'Person has unfullied orders'
end
return errors.join(',')
end
We’ve simplified can_destroy?
, but we’ve added a new method to Person
. Readability isn’t so simple, is it?
Let’s complicate things further by understanding our abilitiy to change code.
Ability to Change
When changing code, you often want to know where to make the change, but you also want to keep the scope of the change as small as possible. Readability in general, and the measures we’ve outlined above in particular, affect this greatly. If we have one giant routine, we know where to make the change, but if we have many single-purpose classes instead, the scope of our change is smaller. Are there other aspects of our code that affect this?
In general, coupling is an indicator of the scope of a particular change. If two classes are tightly coupled, it means that a change one is likely to necessitate a change in another. Further, a class that is coupled to many classes is going to result in a system that is harder to change. This is the basis for the “Law of Demeter”. Code that “violates” this law is coupling itself to more classes than code that doesn’t “violate” the “law” and is thus harder to change.
The dependencies between classes are a good indicator of coupling.
Fan out
A class or method that uses a lot of classes or methods to do its work has higher coupling than one that uses fewer. This is often referred to as fan out, and it means that the class in question is more likely to have to change when the classes or methods it uses change.
Fan in
Conversely, a class or method that a lot of other classes or methods use also has high coupling. Obvious examples in a Rails app would be a central model object (like a person or an order), or helper methods in ApplicationHelper
. These objects and methods get used everywhere, and thus are very hard to change, because a change can have a ripple effect through the system.
Notice again how these conflict with other attributes of readability. A routine that is quite large, but has no external dependencies has almost no coupling, but could be hard to understand, since it is long, potentially having many variables and many paths through the code. Code spread across many single-purpose classes in a loosely coupled way will be easier to change, but potentially harder to understand.
Better
So, what is “better” code? I don’t think we’ll ever have a fool-proof way of figuring this out, but we do have objective measures we can use to better explain why we think one piece of code might be better than another. Next time you’re reviewing code or doing a refactor, instead of relying on a gut feel of “better”, jot down where the code stands along measurements like these. How does the new code compare to the old? The answer might surprise you.