Avoiding Phantom Code
Phantoms, ghouls, spectres; semi-transparent demons and star antagonists of your horror story du jour. So much of their frightfulness stems from their intangible nature. People see them and know they exist, but to put your finger on one seems all but impossible.
Thus is the problem with phantom code. It exists, it may work perfectly, but it is not all there. So, with its implementation unclear, it can be a time suck for developers new to the system.
In my experience, there are three major categories of phantom code. In each case, we can find simple changes to make our software more clear, expressive and maintainable.
The invisible function call
Invisible function calls are functions you can see defined, but have trouble identifying what code/action invokes them.
HTML and Javascript have become notorious for the invisible function call. Action handlers are being moved out of the HTML and into Javascript .on()
calls. This slashes the information a dev can gain by inspecting rendered HTML, or even within the view code itself. Adding clarity by assigning handlers directly to their elements is a great way to keep developers sure about the behavior of a layout.
<!-- Unclear functionality -->
<span>Add user</span>
<!-- Obvious functionality -->
<span onclick="addUserRow()">Add user</span>
As we trend away from HTML to rely more on Javascript, we are losing some of the intended expression in the languages. This is a dangerous trend.
The function you never saw defined
One of the biggest problems with global functions is finding where they are defined was never easy. With the ability to monkey-patch existing object, we can run into similar problems.
Rails gems are often guilty of this. Gems exist outside of a codebase, but can monkey-patch existing objects. This allows a function to be called without the function appearing to exist at all. This can be enormously frustrating to developers unfamiliar with your gem list. I have watched developers spend hours hunting for functions that appeared to be local but were actually defined externally, within a gem.
Here is some actual code i've worked on. Notice the geocode function is nowhere to be found. In fact, even if you searched for it, it doesn't exist in the codebase at all.
class Address < ActiveRecord::Base
attr_accessor :zip
def get_lat_long
geocode()
end
end
So much of this confusion can be avoided with simple comments. If an unnamspaced function from a gem is called, comment it as such.
def get_lat_long
# Defined in the Geocoder gem
geocode()
end
The 15 seconds it will take could stand to save minutes or hours down the line. At that rate, it might be the most profitable line of code you write for months.
The dynamic function call
Dynamic function calls, as well as dynamic attribute assignment, are simply bad practice. The security and maintainability risks are rarely, if ever, justifiable.
In the best case, we are calling a function without having the function call exist in a searchable fashion within the code. In the worst case, we are calling a function or setting a property dynamically based on user input. If you find yourself here, may He judge you mercifully.
In both cases though, clarity of code is reduced. Consider the following code.
method_to_call = "replace_#{object.class}"
send( method_to_call )
Now, what if a developer is tasked with making a breaking change to the replace_string()
function? They need potentially need to fix every point where that function is called. Will they know it is also being called from the code above?
In my experience, on any sizable system, no. The result will be a bug; hopefully found by QA, but more likely found by a customer. All because a dev used a "clever" way to invoke methods instead of passing arguments.
Express yourself
In each of these examples, while the code may function well, it lacks a critical trait: expressiveness.
I maintain that coding better does not mean writing faster code, or shorter code. I'd argue it doesn't even mean writing bug-free code. It means writing code that is more maintainable. After all, even the highest quality, fastest code crumbles at the hands of a future, confused developer.
Great code stays with a company long after the developer who wrote it. This means that when that developer leaves, the ability to easily understand their code didn't walk out the door with them. One of the best ways to do this is to keep your code phantom-free.