30 September 2023
A story about responsibility, mental load, design, empathetic coding and traffic lights. Buckle up.
Original opening narrated by ChatGPT for dramatic effect.
It's 8 a.m. in Christchurch, New Zealand. The air is crisp, carrying the invigorating chill of winter. You're on your daily pilgrimage to the office, striding purposefully with headphones snugly in place, drowning out the world with your favourite tunes. As you approach an intersection, a familiar conundrum beckons - the eternal question that plagues the minds of all software engineers.
"Do you press the crosswalk button?"
Ah, but for us, the elite of the coding world, the answer is a resounding "it depends…"
As software engineers, we exude competence, and know that hammering the crosswalk button is about as impactful as shouting "Hello world" into the void. The action, you see, is idempotent. So, we logically dismiss the notion of frantically jabbing at it multiple times. Thus, we are left with two clear-cut options: to press or not to press.
But hold on, before you make your choice, let's dissect the situation further. You see, traffic lights in Christchurch pull a little trick between 1 a.m. and 6 a.m. to ease the night's burdens. But, it's not the dead of night when traffic lights don their nocturnal persona. It's 8 a.m. and the default crosswalk behaviour reigns supreme.
Ever observant, we notice that the crosswalk signal is unlit as if taunting our decision-making skills. As astute engineers, we know this particular traffic light has been struggling with maintenance issues. Alas, the red light remains dormant, and only the ever-reliable green light is doing its job.
And then, the pièce de résistance! We glance across the street and spot fellow pedestrians. They, too, seem to have engaged in the ritualistic button pressing while the crosswalk signal remains stubbornly unlit. Ah, the odds are in our favour. As seasoned software engineers, we confidently conclude there's no need to push the button.
Just as we revel in our decision, another commuter joins our side. In a decisive move, they push the button. As we anticipated, the crosswalk signal remains indifferent, displaying no hint of crimson. Aha! We knew it all along – the light is, in fact, malfunctioning. Oh, the folly of our fellow traveller!
With the self-assurance that only software engineers possess, we sense the impending green light. It's a countdown we're all too familiar with – 1… 2… 3! And like a scripted sequence in our meticulously coded program, the crosswalk instantly bathes in green. Boom! We, the unsung heroes, have once again saved the day. The answer was crystal clear: pushing that button was unnecessary.
With a triumphant smile, we step onto the crosswalk when a hand suddenly grabs our arm and pulls us back onto the pavement as a car speeds through just in front of us.
Here are code drafts of our logic and our life saver, respectively
class Developer < Person include UrbanWalker def cross_the_street cross_light_button.press if should_press_button? until cross_light.green? do cross(road) break end end private def should_press_button? # Our worldwide understanding of traffic light rules TrafficLightButtonRules.should_press_walk_button?(self, time: Time.current) end end
class LifeSaver < Person include UrbanWalker def cross_the_street cross_light_button.press until cross_light.green? && road.safe?(traffic) do cross(road) break end end end
We can see that we definitely know too much about how the traffic light works.
We've added much code to optimise our procedure from unnecessary tasks, like pressing the button. The
#should_press_button? method could be a simple if/else statement instead of the current
TrafficLightButtonRules module; the question would remain the same.
Is it our responsibility to know the button's status and how it works?
Even if pushing the button resulted in an expensive or time-consuming outcome, who is more likely to understand when to perform that costly action: you or the traffic light?
Advice #1: Let the code run
Code procedures aren't subject to forgetfulness, but developers writing the code are. We spent a lot of unnecessary time developing and maintaining
TrafficLightButtonRules, which can be distracting. We've now inherited new code to maintain. We have to dedicate some of our attention to these rules. The brain must ensure the button rules are up-to-date and well-tested. And in the story, we increased our chance of focusing on the wrong thing and endangered our life.
Removing these pressing rules and applying the same behaviour when facing the button is liberating. The person who systematically presses the button gets more attention span to focus on more critical aspects of road crossing.
Advice #2: Seek less logic
The traffic light is well designed and can handle multiple presses, considering we TRUST the traffic light to function correctly. Imagine a traffic light with the following features:
Here is an incentive to know every traffic light's behaviour and button status. People may need to develop strategies to tell whether someone has already pressed the button. This example demonstrates terrible design and would be a pain to use.
Knowing too much about other objects for the code to function correctly smells like of bad design. The current design of traffic lights is excellent. We don't need to know whether to press it; we can push it every time.
Advice #3: Design with trust in mind
Let's pause for a minute and think about reasons not to press the traffic light button:
We could group these reasons into two categories: I can't or don't want to push the button. These reasons show it doesn't depend on the walk button status at all but on our capability to press it.
It depends on my status, my ability to push the button and not on whether the button should be pushed. Often I can do it.
Advice #4: Execution context should mind its own business
Have you seen this type of code before?
class Post < ActiveRecord::Base def publish update(published: true) unless published end end
What was the intent here?
We thought calling
#update would result in some side effects like changing
updated_at even though
published is already
true. Maybe we were scared the database would get an unnecessary hit. We do not trust the code to behave appropriately.
But, just like the walk button,
#update is well designed. You don't need to know the status of post to update it, you do. ActiveRecord won't apply the update when no attribute has changed. Nothing really stops us from executing the code. Therefore we should call the update method.
This method doesn't prevent the execution of code, has less logic and assumes trust in
class Post < ActiveRecord::Base def publish update(published: true) end end
The next code blocks are inspired from an AppSignal article: Three Ways To Avoid Duplicate Sidekiq Jobs. The difference is that I'm using ActiveJob instead of Sidekiq.
In this example, we want to prevent a job from being scheduled multiple times. One option suggested is to have two flags that
BookSalesService can query.
Disclaimer: I don't know whether the following code blocks work. Take it as pseudo code more than anything.
# 1. DIY section module BookSalesService def schedule_with_two_flags(book) # Check if sales are being calculated right now return unless book.sales_enqueued_at > book.sales_calculated_at book.update(sales_enqueued_at: Time.current) BookSalesWorker.perform_later(book.id) end end class BookSalesWorker < ActiveJob::Base def perform(book_id) crunch_some_numbers(book_id) upload_to_s3 # New adition book.update(sales_calculated_at: Time.current) end # ... end
The intent is sound yet the implementation can be improved. The code is written so that
BookSalesService wonders whether it should queue the job instead of queuing the job.
BookSalesWorkertrusting it from performing the job twice?
BookSalesService's responsibility to control the worker's queuing?
Here is what it could look like if the service was minding its own business and trust the worker from doing what's right.
module BookSalesService def schedule_with_two_flags(book) BookSalesWorker.perform_later(book.id) end end class BookSalesWorker < ActiveJob::Base around_enqueue do |job, block| book = Book.find(job.arguments.first) if book.sales_enqueued_at < book.sales_calculated_at book.update(sales_enqueued_at: Time.current) block.call book.update(sales_calculated_at: Time.current) end end def perform(book_id) crunch_some_numbers(book_id) upload_to_s3 end end
This is an improvement, as only the worker needs to know about the two flags on books table now. The
around_enqueue block makes the intent clearer, and the
#perform action is free from any confusing and unrelated logic. Can it be improved further? Probably.
This post is part of a bigger idea I'm trying to validate and crystallise around business logic and responsibility. I'm sorry it's a mess, and if you made it this far, you're probably wondering what to take from it. Here are more confusing thoughts about empathetic coding instead.
This might sound ridiculous, but I'm subject to anthropomorphism when coding. I see little beings (objects) in a company (codebase) only trying to do their job the best they can. Here is a weird suggestion: envision objects as colleagues and understand how they would collaborate.
Just like you would try to understand another's point of view in an argument or debate, try to understand the current context:
self of a method. Zoom in the method and guess whether the collaboration would make sense and work in real life.
In the previous example, put yourself in the Service and Worker's shoes.
BookSalesService, I have good intentions. I aim to save time and prevent unnecessary work for my colleague by implementing a two-step process. I have many responsibilities, so a comment explaining this part of the process is necessary.
BookSalesWorker, I might feel slighted. Am I too dumb to check whether the book had its sales numbers calculated recently? Why are we even doing this two step check, it's inefficient.
At work, would you rather be asked to solve a problem or monkey code someone else's solution word for word? A leader would confidently give you a task, knowing you can solve it independently. A micromanager will review your code and ask for changes until it matches the solution they have in mind.
I have no control over the leadership strategies of my actual manager, but I can be a great leader and design these little objects so that collaboration is healthy in their world.
Do you write code like a leader or a micromanager?
There is no definitive answer to the question "Does pushing the crosswalk button actually do anything?". Depending on the location, the time, the type of crossing, it will behave differently, but statistically it seems that most crosswalks are automated in big cities. Here are some fun facts about traffic lights:
The world's first traffic light was installed in London in 1868, but it was not automatic. Instead, it was manually operated by a police officer.
Japan has blue traffic lights in some areas because historically the Japanese used the same word for green and blue.
Red and yellow cards in FIFA were introduced after a referee was stopped at a traffic light and thought "Yellow, take it easy; red: stop, you're off". They also helped to address the communication issues due to the various languages involved. ref
The only upside down traffic light in the US is located in Tipperary Hill, Syracuse, New York video
The narrowest street in Prague has its own traffic light for pedestrians. video