Navigate back to the homepage

Difficult but simple solutions versus easy but complex ones.

Corentin Bilotta
August 29th, 2020 · 3 min read

We all know the feeling. You’ve been struggling for hours or even days on a problem. After trying every possible thing you could come up with you ask someone you know is an expert in the domain.

You know what happens next.

That persons comes, does about 12 seconds of thinking, writes two lines and clicks two buttons, and the problem is gone. You feel admiring, but also very dumb.

The ability to solve difficult problems with a seemingly simple solution is probably the best indicator of domain expertise.

Experienced programmers will be able to find more simple (but not easy) solutions to difficult problems. Less experienced programmers will have a tendency to find easy, but not simple solutions to difficult problems.

Here is a very basic example of a simple but difficult solution to find versus an easy but complex one:

A simple bold formatter.

Let’s say we have a text containing portions that should be represented in bold in your website. Those sections are between two asterisk symbols, as such:

1Not bold portion and **bold portion**

To have the bold text actually display as bold in a webpage, you have to replace the asterisks by the HTML tags, like this:

1Not bold portion and <strong>bold portion</strong>

To achieve the expected result, you could use something like this:

1const text = 'Not bold portion and **bold portion**';
2
3// First, let's find the index of our first asterisks so we know what to replace.
4const indexOfFirstAsterisks = text.indexOf('**');
5// Now, let's replace the first ones with an opening tag (<strong>).
6const textWithOpeningTag = text.slice(0, indexOfFirstAsterisks) + '<strong>' + text.slice(indexOfFirstAsterisks + 2); // 'Not bold portion and <strong>bold portion**'
7// Let's find closing asterisks index.
8const indexOfLastAsterisks = textWithOpeningTag.indexOf('**'); // 41
9// And replace them by the closing tag
10const textWithStrongTags = textWithOpeningTag.slice(0, indexOfLastAsterisks) + '</strong>' + textWithOpeningTag.slice(indexOfLastAsterisks + 2) // 'Not bold portion and <strong>bold portion</strong>'

If the text changes and contains more asterisks, you would need to throw this logic in a loop, and do all sorts of modifications. If you remove the closing asterisks, it simply breaks.

Now, I know you could probably figure out something cleaner very fast and this example is bit exaggerated (although, not always, in my experience), but it does illustrate my point well:

This code is very inelegant. But it was easy to write. It required very little thinking. You can write it by just following simple “replace the asterisks two by two” logic.

But the problem is, even though it is EASY, it is not SIMPLE. Any junior programmer can probably fix this problem this way. But you can probably agree you don’t want this type of thing in your code.

Here is one of the solutions a more experienced programmer might have used:

1const text = 'Not bold portion and **bold portion**';
2
3const textWithStrongTags = text
4 // First we split it everytime there is a group of two asterisks into
5 // an array of separate pieces of text.
6 .split(/\*\*([\s\S]*?)\*\*/g) // [ 'Not bold portion and ', 'bold portion', '' ]
7 // Second, we loop on this newly created array, and because we know
8 // that every pair element will necessarily be after an "opening" asterisk group,
9 // and the next element, after a "closing" asterisk group
10 .map((part, index) => (index % 2 ? `<strong>${part}</strong>` : part)) // [ 'Not bold portion and ', '<strong>bold portion</strong>', '' ]
11 .join(''); // 'Not bold portion and <strong>bold portion</strong>'

It will not break if you add more bold sections, it’s cleaner and uses less lines and instructions. It will also not break if you do not provide closing asterisks.

As you can see, this code is pretty easy to understand, if you do understand the language enough and know how split, map, and modulo work.

But coming up with it is a more difficult. The thing is, it might have taken more time to think about it, but it took less time to implement than the first solution.

And the result is a more robust, cleaner, and overall just a better solution.

Now, let’s get into a more difficult problem.

Sequential loading.

Let’s say you are making a nice fullscreen slider in your website’s homepage.

In order to have a snappy website, you want to avoid loading every image at the same time. You decide to load the images one at at time:

First image starts loading instantly. When the first image is done loading, then we start loading the second one, and so on.

For the sake of simplicity, ask the programmer to simply code some logic that is responsible for loading images in sequential order. You should ask for the simplest possible code.

No need to do anything with them, not even display them. We just want to fetch an image, wait for it to be fetched, and move on to the next one without doing anything with it.

Here is an example of how you could do this:

1const images = [
2 'https://source.unsplash.com/random?i=1',
3 'https://source.unsplash.com/random?i=2',
4 'https://source.unsplash.com/random?i=3',
5]; // Let's just go for a few random images from Unsplash.
6
7class ImageLoader {
8 constructor(images) {
9 // Storing the images in an object shape containing a key to know
10 // if they have been loaded or not.
11 this.images = images.map(image => ({ url: image, loaded: false }));
12 this.loadNextImage();
13 }
14
15 loadNextImage() {
16 // Finding an image with loaded property still set to false.
17 const notYetLoaded = this.images.find(image => !image.loaded);
18 // If we didn't find any then the job is over, so we return.
19 if (!notYetLoaded) {
20 console.log('All the images are loaded!');
21 return;
22 };
23 // If we found one then we to load it.
24 console.log(`Loading ${notYetLoaded.url}`);
25 fetch(notYetLoaded.url, { method: 'GET' })
26 .then(() => {
27 // We don't do anything with the loaded image but we mark it as loaded.
28 this.images = this.images.map(image => image.url === notYetLoaded.url // Looping over the images
29 ? { url: image.url, loaded: true } // If it's the current image then we mark is as loaded.
30 : image // If it's not then we don't modify it.
31 );
32 // Now we start over with the next image by calling this function again.
33 this.loadNextImage();
34 });
35 }
36}
37
38new ImageLoader(images);

This code works. It does exactly what we wanted to do.

But it’s not very simple. We used a bunch of boilerplate to get to our result. There are ways to do better and shorter code.

Given enough experience, programmers will be able to use tricks to drastically increase both code simplicity and the time it takes to write a solution.

Here is such a trick:

1const images = [
2 'https://source.unsplash.com/random?i=1',
3 'https://source.unsplash.com/random?i=2',
4 'https://source.unsplash.com/random?i=3',
5]; // Let's just go for a few random images from Unsplash.
6
7images.reduce((acc, img, index, arr) => {
8 console.log(`Loading ${img}`);
9 const newAcc = acc.then(() => fetch(img, { method: 'GET' }));
10 // Let's add a cute log at the end.
11 return index + 1 === arr.length
12 ? newAcc.then(() => console.log('All the images are loaded!'))
13 : newAcc;
14}, Promise.resolve());

If we were to remove the logging:

1const images = [
2 'https://source.unsplash.com/random?i=1',
3 'https://source.unsplash.com/random?i=2',
4 'https://source.unsplash.com/random?i=3',
5]; // Let's just go for a few random images from Unsplash.
6
7images.reduce(
8 (acc, img) => acc.then(() => fetch(img, { method: 'GET' })),
9 Promise.resolve(),
10);

You can see here that we are reducing the number of lines by a factor of 3, and producing exactly the same result.

This solution is simple and elegant, but not easy to come up with. You need to mix together multiple properties of the programming language to end up with this.

Obviously, there are different solution with different drawbacks to this problem, but generally, a more experienced programmer will come up with this kind of shorter solutions.

If you are running an interview process, I advise you compare the code from different interviewees and see how simple their solutions are.

There is rarely a single solution to a problem. But ask the interviewee to come up with the simplest code possible, compare with others, and figuring seniority level will not be that difficult.

To learn more about how to run developers technical interviews, take a look at the other posts from this series:

  1. Identifying magical thinking during a developer interview
  2. Checking understanding of programming languages building blocks
  3. Difficult but simple solutions versus easy but complex ones (this article)

More articles from Corentin Bilotta

Checking understanding of programming languages building blocks.

Let's start with the basics.

August 2nd, 2020 · 5 min read

Identifying magical thinking during a developer interview

Rational thinking in difficult situations.

June 14th, 2020 · 7 min read
© 2020 Corentin Bilotta
Link to $https://github.com/cbilottaLink to $https://www.instagram.com/corentinbilottaLink to $https://www.linkedin.com/in/corentin-bilotta