The "Simple, elegant type system is all you need" bias

András Tóth - Feb 19 '22 - - Dev Community

This Spaghetti code article will be on the soft side as in soft-skills as I am going to talk about a very frequent and very typically engineering bias that leads to spaghetti code. This article is about a human bias, it is not a JS article nor React though all my examples will be in this language or in a pseudo-web framework.

"Do not disturb my circles!"

Originally from Latin ("Nōlī turbāre circulōs meōs!") it is said to be a quote from Archimedes, almost his final words, shouting at an invading Roman soldier while he was minding his geometrical studies in the sand.

The "Do not disturb my circles!" bias is when we assume we can provide an elegant type system of which can cover all use cases. When the real use cases show up, instead of adopting or changing the system completely, we push back, question or hack these new requirements into the no-longer beautiful type system.

Maybe this bias has a better name, however I am not literate enough in the psychology of biases to know it, so hereby I will name it like this.

Examples

Let's say the engineering team is presented a handful of design slides. Every slide is covering some problems the users face: there is a title, a description and an action to take in form of a button.

Engineer A will now propose that "Uh um, very easy! All I need is this simple structure:"

const userSolution = {
  title: 'Some title',
  description: 'This is where the description will be.',
  action() {
     // TODO
  },
  actionText: 'Press the button'
};
Enter fullscreen mode Exit fullscreen mode

They then proceed and create an entire system based on the assumption that in fact this is the type at heart. Multiple layers of tests are written. The layout is set in stone:

<slide-title>{title}</slide-title>
<description>{description}</description>
<button on-click={action}>{actionText}</button>
Enter fullscreen mode Exit fullscreen mode

Weeks later the UX report comes back:

"We tested the prototypes and a couple of changes will be needed:"

  • One slide will have an alternative action
  • One slide will not have a description or title but an entire video instead and a link below
  • One slide will have a clickable icon instead of the button, no text
  • An there will be a "rich slide", where an interactive custom built "widget" will be placed between the title and the description

The reaction

I have seen many reactions to this scenario, most of which is toxic to the codebase or to the product:

  • Denial: "Is this really what people need? They will get confused because the system is so varied now!" The more confrontative devs will do it, with the occasional deep sighs, that they are forced to ruin their code with this mess.
  • Condition-mania: in which every possible property is added as an optional value. The entire codebase is now a mess of if-else and description && <description>...</description> blocks, it is hard to see what the end result will look like
  • Chicken typing 🐥: it's like 🦆 duck typing just worse: the duck typing is based on flimsy guesses, existing properties are reused with totally different meaning, say if the title has the word video in it, then it must be the video slide: if (title.contains('video') { slide.description = <embed video={slide.decription} />
  • Math cowboy: finds the greatest common divisor of all types and actions and runs with it. Looks smart at first but totally obfuscates any system. (See below).

Sometimes all 3 will appear, so there is a product compromise like a "Description" header remaining on the page even though there is now clearly a video being embedded there. The code is littered with ugly conditional-fixes and superfluous guesses what to do based on a moving target.

The math-cowboys

Let's see an example:

// OK, so now my structure can look like anything
// then use Map
const textSlide = new Map();
textSlide.set('title', 'This is smart slide');
textSlide.set('description', 'This is smart description');
textSlide.set('action', () => {});
textSlide.set('actionText', 'Press the button');
Enter fullscreen mode Exit fullscreen mode

Looks smart, but it is extremely hard to use: for every property now you have to test whether it exists. You will never be sure how many different slides exist since the real world handful cases are now replaced by infinite possibilities. The system now must be carefully analysed before anything is changed.

And why? The math-cowboy did not want to get bothered adjusting their system later.

Fun fact: I knew a guy who ignored the class system of Java and used Map<String, Object> map = new HashMap<String, Object>(); to cover every case.

Polymorphism? Ha! That is so constraining. Let lesser people work instead.

One possible solution

Generally I think it is a good stance to write simple and easy to refactor code when the user needs are not properly understood yet. No need to write future-proof, but instead something straightforward and easy to change.

That way when change comes you will be OK with the idea to redo your system grounds up maybe.

In the concretion with the case above my battle tested solution is to anticipate early the polymorphic nature if real world cases with a trustable duck typing system in place.

I have written about this in my Toxic optionals article, but here's a very short refresher if you don't want to click.

First iteration

enum SlideTypes {
  Text,
}

type TextSlide = {
  type: SlideTypes.Text;
  title: string;
  description: string;
  action: { 
    text: string;
    effect: () => {};
  }
};
Enter fullscreen mode Exit fullscreen mode

Second iteration

enum SlideTypes {
  Text,
  Video,
  AlternativeAction,
  RichSlide,
}

type TextSlide = {
  type: SlideTypes.Text;
  title: string;
  description: string;
  action: { 
    text: string;
    effect: () => {};
  }
};

type VideoSlide = {
  type: SlideTypes.Video;
  videoUrl: string;
  action: { 
    text: string;
    effect: () => {};
  }
};

type AlternativeAction = {
  type: SlideTypes.Text;
  title: string;
  description: string;
  mainAction: { 
    text: string;
    effect: () => {};
  };
  alternativeAction: {
    text: string;
    effect: () => {};
  };
}

// ...
Enter fullscreen mode Exit fullscreen mode

All these slides can now get a sub-component where there is no conditional magic and are short and super easy to read.

And later on the page when you need to output the specific slides you just do a good ol' switch-case (I know it's very old-school):

switch (slide.type) {
   case SlidesType.Text:
     return <text-slide data={slide} />;
   case SlidesType.Video:
     return <video-slide url={slide.videoUrl} action={slide.action} />;
    // ...
}
Enter fullscreen mode Exit fullscreen mode
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .