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 #update
.
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.
BookSalesService
queued BookSalesWorker
trusting it from performing the job twice?BookSalesService
's responsibility to control the worker's queuing?BookSalesWorker
somewhere else?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. Anyway…
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.
Traffic lights weren't always three-coloured. The Invention of the Three-Light System was by a Detroit police officer named and William Potts and later patented by Garrett Morgan in 1923 ref, ref
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