In my previous article I was talking about why simplicity matters and in particular how a great language/tech can be the wrong one for the problem at hand.
This article is about the classes of problems and how they can lead to spaghetti code.
Working against coding tools: "Help us so we can help you"
In my opinion every principle, pattern and tool is part of clean code
if they increase your confidence and safety making changes to a codebase. The guides, techniques, trips and tricks aim at "lowering the cognitive load" so you can spend the freed mental space to laser focus on the problem at hand. In other words, clean code
shifts time spent on piecing together what the other dev did to figuring out what should be done.
Following this trail the tooling around code also save time and mental energy, by
-
catching broken relationships: syntactical and semantic checks at compilation time or in your
IDE
; e.g. if you have made a typo while accessing a property - remembering all requirements: well defined unit tests
-
guarding the flow of data: like type-checks when you pass parameters (in case of
TypeScript
) -
keeping track of all references to a piece of code (in most
IDE
s calledFinding usages
) - code analysis: from quantitative, performance analysis to linting
- code optimization: a well equipped compiler might outperform even a senior developer on code optimization, given you are not standing in the way of it.
You can unlock all these benefits if your code is built with these tools in mind.
As a side effect they will increase your productivity by decreasing the time needed to cross-reference code, i.e. opening a lot of files to check the implementation details.
Now let's see an example where good intention to have additional guarantees leads to breaking many of the tools above.
Immutability vs. JavaScript
object
s
If you have ever worked with Redux
you might have encountered the problem of the lack of immutable compound structures in JavaScript
.
If you are unfamiliar with this problem I suggest reading Why Redux need reducers to be “pure functions”.
Let's just freshen this up with a super short code example:
const nestedValue = 'nested value';
const state = { stringStuff: 'stuff', deepStuff: { nestedValue } };
// let's try to make it immutable
Object.freeze(state); // note: this just mutated your `const` object!
state.stringStuff = 'Cannot do this'; // ERROR - in strict mode, ignored otherwise
state.deepStuff = {}; // ERROR again, can't set a new object reference
// seems we are done, but let's investigate the object referenced by `deepStuff`
state.deepStuff.nestedValue = 'But you can this'; // no error - hmm
state.deepStuff.nestedValue === nestedValue; // FALSE - OMG, what have I done
One can argue that it is possible to recursively freeze
every nested object; but since the plain old object
of JavaScript
is super flexible you will have edge cases, like objects holding circular references 😐.
What's the moral of the story? JavaScript
was not designed with immutability in mind. It was also not designed with object-oriented programming in mind and nor with functional programming.
If we want them we need some extra help. Enter immutable.js
.
Getting immutable nested objects while losing something else
Let's check adapt an example straight from their documentation:
import { Map } from 'immutable';
const nestedValue = 'nested stuff';
const state = Map({ stringStuff: 'stuff', deepStuff: Map({ nestedValue }) });
const newState = state.setIn(['deepStuff', 'nestedValue'], 'immutable yay');
// the lib guarantees this way that we did not change `state`
state.getIn(['deepStuff', 'nestedValue'] !== newState.getIn(['deepStuff', 'nestedValue']);
// TRUE - no more headaches, or...
We now have guaranteed immutability. But we replaced the meaningful object binding
s with string literal
s. We had a headache because of possible mutations and now we have a refactoring nightmare as we now our object's API! 😐
We clearly broke our object bindings
by stringly typing them!
We obfuscated the relationship between the object property
state.deepStuff
and the string'deepStuff'
! Weliteral
ly turned the help off of most of our tools!
Since string literals
are simple values they can be anything! Whenever you deal with strings remember Let's see these examples:
// no errors in any of these cases:
// Did you find the typos? Your code reviewer might also miss them!
state2 = state.setIn(['deepSutff', 'netsedValue'], 1);
// string literals can be anything, like your friend's phone number or a date!
state2 = state.setIn(['+36 (12) 3456756', '2020-05-09'], 1);
// they can be really 'fuzzy' (see also: 'fuzz testing')
state2 = state.setIn(['😐|{}_+]`', '開門八極拳'], 1);
So to recap: we reached the zen of immutability
but we broke most of our tooling, so now we...
- have no code completion => prone for typos
- have only runtime errors
- need to do full text search to see who is depending on our structure (good luck finding
deepSutff
by searching fordeepStuff
) - have to be extra careful with refactors, since no tool will warn us about broken references
In short we replaced the problem of mutability with the problem of brittle code.
Mitigating the wrong problem class
issue
Before enforcing a pattern on your codebase make sure you understand the trade-offs it brings, and then think about the possible frequency and severity of the problems solved and caused by said pattern.
Theoretical benefits must be weighed against the tooling you have and the skills of your development team.
In my example above, I'm pretty sure accidental mutations of objects happen less frequently than renaming or looking up objects and their properties. So a codebase which does not require the special features of immutable.js
might be better off without it. Luckily in this particular there are alternatives which do not break object binding
: check out immer.js.
But if you don't have an alternative you can still box the ugliness by utilizing it in only a handful of mission critical cases.
That way you can also create wrappers around it, so it is easy to replace the implementation in a later time when the better alternative already surfaced.
Remarks about stringly typed API
s
If you have any influence over a future library then please never design an API that depends on string literals
for meaningful business. Remember, string literal
s are values that should not point to objects but should be used for labels in user interfaces, paths for files or data stored in databases.
Bonus: my favorite Angular 1 tooltip fail
This is how I lost an entire working day on the anti-pattern combination of stringly typed
and swallow the error message
. (Sorry, this is gonna be an HTML example, not a purely JavaScript
one). Product wanted a little tooltip to appear over a <button />
on mouseenter
events. I was using angular-uib
library to implement it and it did not want to work - also it did not output any errors.
<!-- This does not work, NO ERRORS -->
<button
uib-popover="Hello world!"
popover-trigger="mouseenter">
Mouseenter
</button>
<!-- This works -->
<button
uib-popover="Hello world!"
popover-trigger="'mouseenter'">
Mouseenter
</button>
Did you see the problem? No? I have tried mouse-enter
, mouseEnter
and everything in between.
The right way was to put the
mouseenter
into single quotes inside the double quotes, as it was meant to be astring
. 😐
Thanks for reading this article!
And if you have any comments, especially if you want to improve the grammar of this post let me know; I am not a native English speaker, so I am super grateful for any stylistic suggestions!