Let your code tell a story

Anibal Ambertin
Eat. Surf. Code.
Published in
5 min readJan 14, 2022

--

New year, new tech blog. But disregarding my (past) lack of affection for blogging, I have been telling the same stories to developers and dev teams around the world as far in time as I can remember.

So here goes the first one: let your code tell a story.

The principles

We all know these principles, yet they don’t seem to be as widely adopted as many of us would like to think. Many of them can be reduced to just two: think of your clients, and think of your family.

Think of API design (think of your clients!)

I’m old enough to call API’s to any interface/contract exposed that anyone else (or myself) can use to consume a service provided by the code behind it.

It is your job, not only to make sure your code does what it’s supposed to, but to make it easier for those “clients” to consume.

One of the good things (and perhaps the best one) about TDD is exactly this. It forces you to think of the interface you expose on one hand, and the decoupling mechanism you need to put in place to make a component testable.

But way before deep-diving into hard-line practices, there’s a few things we can look after to gain a lot from very little effort.

Don’t cheap out on characters

I am not going to rant on this post about why you should not use x, y, z as function names or for long-lived variables (or even worse, use a soccer star name for a component that participates in an HR process… I’ve seen that one once).

But I have some good news for you… characters in interfaces (whether it’s the service name or the contract signature / interface) are free of charge! I think you already know what I am talking about, but I’ll explain a bit further anyway.

Although you might think that updateCustAvatar is a good name for your service (and remember, service is anything “exposed/exported/published” through any means), I’ll give you a few examples of why it’s not.

  1. I have just been asked to add a feature to manage custom avatars instead of the fixed list of options we had. We are using GraphQL and now my mutation is going to be updateCustomAvatar . Can you see how this could be confusing?
  2. Cust can clearly be many things that could be brought into the service domain at any moment (could they?…. there are at least 30 words that start with “cust”).

Don’t cheap out on your interfaces

myNotSoAwesomeService(
requiredDependency,
desiredDependency,
requiredArgument,
optionalArgument
) { ... }

This of course is a bad attempt at dependency injection that should be solved using an appropriate package or framework. But besides that, positional arguments, except required by design (e.g.: configuration versus actual payload) tend to not be a cool thing to do in 2022.

Maybe try this:

interface MyAwesomeServicePayload {
requiredArgument!
optionalArgument?
}
myAwesomeService(payload: MyAwesomeServicePayload) { ... }

Besides, for most languages (particularly those that don’t support named arguments) this will help intellisense kicking-in in your contributors’ IDEs, make inline documentation more navigable and provide an overall superior client experience.

I am not going to advocate for zero-documentation a-la old-school smalltalker, but I do subscribe to the idea that your interfaces should be clean and explicit enough to not need to resort to documentation all the time.

Finally, as the cherry on top, doing it this way is definitely more extensible — particularly in a backwards compatible way, and specially when you are trying to do CD or have multiple service clients (or both).

Tell a family story (think of your family)

A family story is one of those that can be enjoyed by the entire family. It’s short and simple enough to attend the attention span of children, it is clear and conveys a message, and it can be the surface that introduces curious minds to further questioning.

To design nice, clean, bespoken interfaces is wonderful for your clients… but what about yourself and your co-workers who have to maintain whatever is under the hood? You should be nice to this second group too.

We live in an era of amazing runtimes, compilers that optimize your code, tree-shakers and light services… if you are writing code like it’s 1999 to save a few function calls with concerns of their cost, you are not being nice — unless you are writing C++ code for a high-frequency trading system or something of the sort.

Let’s write just some code that does something (you try to figure it out).

updCustMasterRec(cust) {
auth = context.getAuth()
if(auth.role !== 'admin' && auth.role !== 'manager') {
throw new AuthorizationException()
}
customer = CustomerModel.get(cust.id) if (auth.role == 'manager' && customer.managerId !== auth.id) {
throw new AuthorizationException('Customer doesnt belong... ')
}
customer.firstName = cust.firstName || customer.firstName
customer.lastName = cust.lastName || customer.lastName
customer.phone = cust.phone || customer.phone
customer.gender = cust.gender || customer.gender
customer.dob = cust.dob || customer.dob
customer.email = cust.email || customer.email
customer = await CustomerModel.save(customer) if (customer.prefNotChan === 'sms') {
smsClient = injector.get(SMSClient)
smsClient.send(customer.phone, 'Your information was ...')
}
else if (customer.prefNotChan === 'email') {
emailClient = injector.get(EmailClient)
emailClient.send(customer.email, 'Your information was...')
}
return { status: 'ok', customer: customer }}

Now this is of course just a very simple example to prove a point — although the point would be better proven by a more complex example, I also have a life besides this post.

But let’s see how this could’ve been done with a different approach:

updateCustomerMasterRecord(updates) {
return await CustomerModel.get(updates.id)
.then(verifyActorCanUpdateCustomer)
.then(updateCustomerDetails(updates))
.then(notifyChangesToCustomer)
.then(ok)
.catch(error)
}
verifyActorCanUpdateCustomer(customer) {
auth = context.getAuth()
if (auth.role === 'admin') return customer
if (auth.role === 'manager' && customer.managerId == auth.id) return customer
throw AuthorizationException('Not authorized to update customer')
}
updateCustomerDetails(updates, customer) {
updates = customer.entries.reduce((out, entry) => {
out[key] = updates[key] || customer[key]
return out
}, {})
return CustomerModel.save(updates)
}
notifyChangesToCustomer(customer) {
// .... you get the point by now
}

At first sight this might seem like more code just due to the addition of more function signatures… but… have a look at the first function. How beautifully does it tell you the story of what happens when you call it? And you are not forced into implementation details you don’t care about if you only need to make a change to allow a new role to modify the customer.

This allows for quick understanding and curious exploration. Facilitates navigation within the IDE and gets you on your way to splitting responsibilities well enough to reuse code.

Final words

Don’t be afraid to leverage your language of choice capabilities to write beautiful code: code that tells a story. Help everyone in your team to join the #storycoding movement — it will bring happiness and joy to all of us.

--

--

Anibal Ambertin
Eat. Surf. Code.

I help start ups and scale ups design, build and scale digital products, services and engineering teams.