DEV Community

loading...

How spaghetti code comes back - solving the wrong class of problems

András Tóth
A developer with M.Sc. in Computer Science. Working professionally since 2010. In my free time I make music and sometimes cook. Also I don't have Twitter. Sry!
・5 min read

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 IDEs called Finding 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 objects

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
Enter fullscreen mode Exit fullscreen mode

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...
Enter fullscreen mode Exit fullscreen mode

We now have guaranteed immutability. But we replaced the meaningful object bindings with string literals. 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'! We literally 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); 
Enter fullscreen mode Exit fullscreen mode

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 for deepStuff)
  • 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 APIs

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 literals 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>
Enter fullscreen mode Exit fullscreen mode

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 a string. 😐

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!

Discussion (0)