Imagine buying a new car, reading the driver’s manual, and taking it on a joyride into the sunset to enjoy its new features in action. Then, unbeknownst to you, the car slows down whenever you look at the speedometer. You take it back to the dealership, like any rational buyer, and request a replacement. The shrewd salesperson who sold you the car tries to explain that what you experienced is actually a feature: The dashboard activates only when someone looks at it to reduce windshield reflections, especially at night. Still baffled, you question the link between the query of speed and the command to reduce it; the savvy salesperson expounds that it’s for your own safety: It’s a side-effect of checking the speed; after all, you have to take your eyes off the road to check the speed, right?
Understandably furious, you take matters into your own hands and decompile the software that controls the car to find the following:
public class Speedometer {
private StatefulOdometer statefulOdometer;
private Controller controller;
/**
* Gets the current speed and applies the brakes if activated by eye tracking.
* @return current speed.
*/
public double getSpeed() {
OdometerDelta delta = this.statefulOdometer.getDeltaSinceLastReading();
double speed = delta.getDisplacement() / delta.getTimeElapsed(TimeUnit.HOURS);
if (this.isActivatedByEyeTracking()) {
this.controller.applyBreaks();
}
return speed;
}
...
Busted! Now, you’re on a mission; you hunt down all calls of getSpeed
and, unsurprisingly, you find out that the vast majority of callers didn’t
expect said method to have a side-effect.
You don’t blame the callers because neither the method’s signature nor its name
indicates that it might do anything else besides returning the current speed!
A few weeks later, you manage to track down the developer who wrote that code. After an extremely awkward introduction, you explain how side-effects can have undesirable ramifications and that coding must fulfill requirements beyond solving the problem at hand; to list a few:
Appalled, the developer argues that documentation clearly states what the method
does. Since one cannot force callers of a method to read its documentation, you
easily refute this argument. That’s why names matter a lot; one can only assume
one thing about a method called getSpeed
in Java — it’s a getter.
Another argument comes to mind: a Speedometer
has a reference
to a Controller
, so it’s acceptable to change its state; have you a rebuttal?
You point out that allowing extraneous access between objects is a design
deficiency. Applying the Interface Segregation Principle
makes it crystal clear which Controller
methods a Speedometer
can call.
Since the role of said sub-interface doesn’t entail changing state, it should be
renamed to ImmutableController
to reflect that. If the programming language
allows for compile-time enforcement of such constraints, like the const
keyword in a function’s declaration in C++.
In a last attempt to
maintain the status quo,
the developer argues that every caller of getSpeed
has to re-implement
the rest of the logic (i.e., test for the eye-tracking activation and apply
the breaks if needed) because that was the requirement. You refer to
the Single Responsibility Principle
and point out that said behavior is the responsibility of another class; you
want classes to be like LEGO bricks: small, robust, and easy to re-purpose as
the requirements change (because they will).
After the long discussion, you rejoice as the developer agrees to make the changes and you get to enjoy your new car without side-effects. As you drive into the sunset, you reflect on the takeaways: make smaller things; each is responsible for only one thing; and, most importantly, care for your code or fear that the next person to read it knows where you live.