loading...
Cover image for Introduction to Pragmatic Beauty

Introduction to Pragmatic Beauty

uyouthe profile image Miloslav Voloskov ・8 min read

TL;DR: The product is beautiful when it’s easy to comprehend, you have a solid amount of time before adding a new feature would require rewriting the product, and you can ship the product in time.

What it is

I believe that the most important thing you can have when building your product is an inner sense of beauty. I don’t know why, but when I rewrite the code in the way it feels like it would be better this way, a major part of bugs magically vanishes. It happens every time.

Okay, we get it, beauty is important, but what exactly is a beautiful project? Is a thoroughly designed complex architecture beautiful? Or if it isn’t, is the PHP app I put together in an evening beautiful just because it works and I built it in such a short period of time?

The concept of beauty is confusing, because even the seemingly broken and dissonant art can be considered beautiful. So is a big pile of messy hacks one on top of the other beautiful then?

The Caretaker - Everywhere at the End of Time

Let’s find out.

What problem does it solve

Cognitive complexity.

The perceived complexity of a software product consists of natural and cognitive complexities. If an algorithm can be rewritten in a more comprehensive way while preserving functionality, it’s cognitively complex. If it can’t, it’s naturally complex.

There are things like CRDT – no matter how hard you try, with existing programming language it’s impossible to make them simple. The idea itself is too complex and requires a solid grasp of conceptual knowledge to realize what happens there by just reading the code.

But the messed abomination of a nested loops and random variable names with random indentation which can be replaced by a single map function is just cognitively complex. Even though the idea behind was straightforward, someone have chosen to make things unnecessary complicated.

What Pragmatic Beauty consists of

I give 60% of it to being able to finish the product in time and the overall concept of “shipping being a feature”. If you never ship the product, all your work doesn’t matter.

I give the other 40% to how fast does cognitive complexity tend to raises when you add features.

The high cognitive complexity was so common that Steve McConnell coined a term of a codebase being “sticky”. Your codebase is sticky when it’s much easier to make a mistake than to make something right. You can’t just focus on one thing – there are always multiple things to keep in mind to write the correct code, and it just doesn’t work well with our brains.

With that being said, let’s define a beautiful product:

The product is beautiful when it’s easy to comprehend, you have a solid amount of time before adding a new feature would require rewriting the product, and you can ship the product in time.

Let’s revisit the initial examples. Complex architecture? The architect probably designed a set of guidelines about how to implement features, there are probably a plethora of classes ready for you to inherit from. But it’s still sticky, it still requires keeping a bunch of things in mind at once. It requires writing code in multiple files just to keep the architecture clean. Shipping in time? You mean building a complicated building pipelines for all that conventions to work? Or implementing all that layers of abstraction in time? The questions are rhetorical.

A think put together in one evening? Sure enough you shipped it in time, but there was certainly not enough consideration on toolkit and approaches so the cognitive complexity would spike quickly as the product would grow.

A dissonant mess? Hard to comprehend, hard to add new features. Is it beautiful? In a weird way, maybe.

Code examples

Consider the following code:

let docs = allPossibleDocs
let indexes = []

for (let i = 0; i < docs.length; i++) {
    if (userDocs.includes(docs[i]) {
        indexes.push(i)
    }
}

for (let i = 0; i < indexes.length; i++) {
    docs.splice(indexes[i], 1);
}

return docs

What is docs? What are indexes? Which array does they belong to? What the hell actually happens here?

const difference = (a, b) => a.filter(x => !b.includes(x))
const docsUserNeedsToFill = difference(allPossibleDocs, userDocs)
return docsUserNeedsToFill

Everything is clear now. We find the difference between all the documents we provide and the documents that user already filled. We return the documents that user needs to fill.

Consider another example:


const getValue = () => {
    let value = response.user.data.remainingAmount;

    if (response.user.data.orderState > 3) {
        value = response.user.data.alreadyPaid +
            localUserData.stillHaveToPay
    }

    if (response.user.isBanned) {
        return response.user.data.debt
    }

    return value
}

getValue()

Not only the order state is confusing, but the spaghetti of getting back and forth between a value and a return statement is tiring. Let’s rewrite:


const getCurrentCase = () => {
    const orderIsComplete = response.user.data.orderState > 3)
    const userIsBanned = response.user.isBanned

    if (orderIsComplete) return 'orderIsComplete'
    if (userIsBanned) return 'userIsBanned'
    return 'default'
}

const getValueBasedOnCase = currentCase => {
    const normalValue = response.user.data.remainingAmount
    const userDebt = response.user.data.debt
    const completeOrderPrice = 
        response.user.data.alreadyPaid +
        localUserData.stillHaveToPay

    switch (currentCase) {
        case 'orderIsComplete': return completeOrderPrice
        case 'userIsBanned': return userDebt   
        default: return normalValue
    }
}

The code is straightforward – you only need to read it from up to down, there are no jumps. We labeled the values so everything is clear now. We also can reuse the currentCase multiple times, I’m pretty sure we’ll be doing other things based on user status.

Notice how the second code snippet is longer than the first one even though it has less spacing.

There is no need to worry about code being too long. The large but cognitively simple code is better than short but Incomprehensible.

Things that will help

Sugary languages

On the one hand, they can seriously reduce the cognitive complexity – just the thing we need. Right?

On the other hand, if you really try, you can create a bloody unreadable mess that is absolutely incomprehensible but somehow works perfectly.

But using the sugar wisely, you can write in one simple line what in less sugary language would require ten. There is a performance penalty sometimes, but as these languages improve, it tend to decrease. I would pick the ability to actually launch the project in time over the small, case-specific performance benefit.

I like that ECMA realizes it and tries to make a new versions of its standard more sugary.

Declarative languages and tools

While often being limited, declarative languages are way less prone to errors than imperative ones. They are easy to validate even with simple tools that are already built in your IDE. In most of them you can even write the lines of code in arbitrary order and everything will work. There will be no effect if you reordered the lines of CSS in the scope of one selector, There will be no effect if you reordered the key-value pairs in JSON parsed by a properly-design tool.

This simplicity is the reason why most of the configuration nowadays is declarative while configuration in the past was often imperative.

Even though our everyday life consists of algorithms where the order is important, declarative thinking becomes simple when you think of the lines of code as being applied all at once – like a two-dimensional approach of wrapping a sheet of paper over an object instead of one-dimensional where you follow the code from top to down.

You’re just defining the properties of an object. If you build an app around that concept and insure proper overriding mechanism just in case, you can achieve a solid product built with a small codebase.

Zero configuration

Starting up a project is already a hassle on its own and there is no need for any additional hassle. There is no need to configure the project thoroughly right at the start.

There is a trend amongst architectural astronauts to bash simple, zero configuration tools, but NPM and Ruby already proved their viability with even the larger projects needing no configuration at all.

I’d pick a zero configuration tool over competitors but the feature of lowering the abstraction level and actually writing some configuration is an important one to have.

Idempotent data model design

Your data model is idempotent when you apply the same operation ten times and the model changes as if it was only one operation. As you grow, your app’s logic would become more complex. There will be more parts of the app, there will be more possible operations.

Bugs related to a data model not being idempotent are extremely hard to catch because you’ll have to count how much times an operation was applied exactly. This lead to an obvious conclusion – a toggle-based logic is a very bad idea.

Instead of toggle-based endpoint like the following:

toggleDarkTheme()

…use this:

setDarkThemeEnabled(boolean)

It doesn’t matter if an app calls the latter function one time or one hundred times in a row – whether the dark theme would be enabled or not wouldn’t depend on its previous state.

Easy deployment

Trust me, you won’t like to worry about the deployment. “That one feature would work now, just one more deploy” can actually take twenty complex deploys one after another.

I believe that trivial products shouldn’t have to be configured at all in order to be deployed, and a non-trivial one should be configured once and smoothly deploy every time after.

Set up that goddamn SSH already. Choose the hosting that allows easy deployment or set up webhooks or even some kind of automation.

I believe that as soon as your pull-request is merged into a main branch, the deploy should happen automatically.

Things that will harm

Too permissive languages

Even though the benefit of almightiness can be helpful in some cases, it needs a little too much thinking to code in these languages while not raising the cognitive complexity exponentially.

Yes, they inarguably gives some performance benefits right at the start, but constantly shipping a marginally slower app is better than redesign major parts of it every time you need a new feature.

Redesigns are inevitable when implementing something in that languages. If you don’t redesign, at first the performance would drop, and later the cognitive complexity of layers upon layers of hacks will render your project incomprehensible.

Too complex and strict methodologies

It is important to remember where they all started. Many of them was invented for the specific use-cases, even for specific companies and their culture. They was designed for years with the idea of how things should work. This alone is a case of “designing in advance” – a clear antipattern.

Following a strict methodology just because you was the one who introduced it is a terrible decision. It can slow down or even cripple the development process. This is essentially a bureaucracy.

Not having a methodology at all when you have more people than just you in the team is another extreme.

I believe that the methodology should be designed just like the good tool – simple at first but also normalizing overrides when you need them.

Any tool that is used for the sake of being used

We decided that we’ll use Redux because I read some blogs and that’s what real companies are doing now.

We use storybook because no one uses it and I personally feel sorry for it and someone have to use it

We’re going to write everything in a functional language because of how pure and beautiful it is. Yes, it’ll require a building of our own interactive charts library from scratch, but the whole codebase needs to be free of side effects.

Our app requires no native APIs but we’re going to build it all native because the native apps are always better, and it doesn’t matter that all our programmers are web developers and don’t know how to build native apps.

You get the idea. The scaring truth that half of the above quotes are real. One of them was said by a VP.

Benefits over Duct Tape Programming

The Duct Tape Programming focuses more on shipping in time. If you focus on this thing only, the cognitive complexity would rise quickly as the product will grow.

I find the Pragmatic Beauty to be neatly balanced between the two extremes being the Duct Tape Programming and the Architectural Astronautics.

Can the “ugly” product from the Pragmatic Beauty point of view be successful? Of course, yes. Can it make money? Yes. Can it be sustainable? With a large enough staff, probably yes. But can you sustain an ugly startup with a small team that startups tend to have? Can a new team members comprehend it in reasonable time? No.

So listen to your inner sense of beauty and embrace it.

Posted on by:

uyouthe profile

Miloslav Voloskov

@uyouthe

🏳️‍🌈 Declarative logic for masses

Discussion

markdown guide