# Write (Code) for Humans, not for Machines
If you’re a Rails developer, constantize
is a very enticing method to use. It
allows you to dynamically call the class you need given a string of that class’
name. Take a look at the following snippet; I’ve also commented the code a lil
bit to help non-Rails developers understand:
class PaymentController < ApplicationController
# The valid options
VALID_GATEWAYS = ['paypal', 'stripe']
def create
# Validate the param. Ensure there aren't any tampering from frontend.
validate!(gateway_name)
# Camelize the gateway name parameter. paypal => Paypal, stripe => Stripe
gateway_name = params[:gateway_name].camelize
Payment.process(gateway_name, params)
end
def validate!(gateway_name)
raise "Gateway invalid" unless VALID_GATEWAYS.includes?(gateway_name)
end
end
class Payment
def self.process(gateway_name, params)
# This is the magic. It constructs the class name on-the-fly.
# If gateway_name = "Paypal", then "PaypalGateway" will be initialized.
"#{gateway_name}Gateway".constantize.new.process_payment(params)
end
end
class PaypalGateway
def process_payment(params)
# process params
puts "Params processed: #{params}"
end
end
class StripeGateway
def process_payment(params)
# process params
puts "Params processed: #{params}"
end
end
With the help of constantize
, we are wiring Payment
class to infer the target
class name based on the parameter we passed to it. No switch-statements or
if-statements needed. So, should there be a new requirement to allow customers
to pay with Braintree, we would just need to create a class called
BraintreeGateway
, include braintree
in the VALID_GATEWAYS
array and make sure
the form from frontend passes braintree
as params[:gateway_name]
back to the
controller. We have our own little convention here. Life’s good, right?
Well, yeah, for now.
It may seem like nothing can go wrong from here. Of course, the code snippet we have here is a very simple example. A typical production application will have a lot more complex code than this. Having these conventions as a subset of your application, just like in this example, may look cool and easy to work with now, but it will come back and punch the team in the gut.
But how will that happen?
Well, for starters, we are dynamically constructing the target class name with
string manipulations. If our new team member Craig, unknowing of our little
convention here, comes in and do a project-wide search for PaypalGateway
to see
where it’s being called, well Craig is out of luck. It would seem as if nothing
from the codebase calls PaypalGateway
.
This also makes our code more fragile. A simple change to the class name, from
PaypalGateway
to PayPalGateway
or from BraintreeGateway
to
BrainTreeGateway
will break the application. An IDE won't be able to help us
catch this error beforehand, because in its knowledge, nothing in the codebase
call these two classes.
Not to mention that this convention becomes another thing that new team members will have to be aware of and learn, since it's something specific to our codebase. This slows things down for them. Ideally, we'd want them to immediately be able to contribute to clearing our Backlog.
If this isn't documented, then chances are six months down the road, we, the creator ourselves, are not going to remember about the mechanics of this part, either.
By making conventions like this in our application, we are creating unsafe waters for ourselves and our team members. We're gonna have to document this convention somewhere. That's kind of ironic; We created this convention so that we can do less work when we need to extend the application. Now, it seems there's more work to do and more things to be cautious about.
# Writing code is writing, so pick your audience right. (Hint: It's your team members)
Remember that writing class you took in college? The teacher kept reminding you to always put audience in mind when writing. The same goes with writing code. We are not writing code for machines. We are writing for humans: for ourselves and for our fellow team members. From the start of our career as a software developer till the end, the tools and the languages we might use vary from one time to another. But in between all these changes, there is one constant:
We are always going to work within a team.
Hence, it makes sense to spend a lot of time improving the way we write code for the team, regardless of what language we use. It will help our team to quickly jump into the code and work on it instead of having to spend halves of the time headscratching on how the heck does this and that thing work?
Put simply, here's the goal we want to strive towards:
Maintainers of the application should be able to comprehend and reason about the code without actually running the code.
It sounds simple, but it is really a hard thing to do. If the code we write requires our team members setting up breakpoints here and there and running the application in debug mode just to see what's the state of the application at runtime or what's the flow of our code like, we have failed.
I am still struggling with this goal. Sometimes I am guilty of writing bad code myself. Sometimes, I let bad code slip into the codebase by not scrutinising my team members' code enough in Pull Requests. But throughout my career as a software developer, here are a few tips that I've learnt based on my experience:
- Good code is simple and dumb
Good code is straightforward. Good code is dumb. Good code is explicit. Good code spits out clearly what it is trying to do. Good code is obvious. Good code is readable and clear.
- Create small objects (or functions)
When you break your code into many byte-sized (pun intended) pieces, your code becomes easier to comprehend. Many design principles push for this (e.g. SOLID principles by Uncle Bob, YAGNI, KISS, etc.). One of my favourites on the list is Sandi Metz' Rules for Developers. It is a really handy guideline to keeping your code organised in small bite-sized chunks. One of my colleagues at ServiceRocket shared this video by Sandi Metz that I think we should all see in order to understand how beautiful it is to always make small objects:
- Use tests to communicate your code's intent.
Apart from warning you of regressions, tests are also specs for your code. Don't be afraid to be explicit in tests. Write clear and concise spec descriptions that will help you and your team members understand the test. Those extra bytes of strings won't go to your end users anyway.
- Do not reassign variables.
This is fundamental in the functional programming paradigm, and it's a good
safeguard against bad programming practice. Reassigning variables will leave
your team members to wonder what's the supposed value of this variable at a
given line of code, and they will have to run the code to know the actual value
at runtime. That is exactly what we do not want. You will realise many
languages have assignment operators that prevent reassigning values to
variables, like Scala's val
and JavaScript's const
. Use them by default. In
languages that don't have this, just always remind yourselves never to reassign
a new value to existing variables.
- Not naming variables clear enough.
If you find naming variables a hard thing to do, you're in the right direction. It shows that you already have the intention to write for humans. Don't take shortcuts for this, and don't be lazy. There are plenty of tools to help you be productive with long variable names. IDEs and code editors nowadays have autocomplete features that can help type these long words for you. There are no excuses for this.
# Good code is hard to write...
…but the good thing is, we don't have to start from scratch. Prominent software developers like Sandi Metz, Uncle Bob Martin, Kent Beck, Martin Fowler all have written for us plenty of books and articles on what we ought to do to write good code. The only thing left for us is to learn all these and keep on practicing.
Good luck!