KISS principle is not that simple

A refresher on its origins and implications on software engineering

Software engineers use acronyms to convey certain ideas in a single word to save time. DRY, YAGNI, KISS and SOLID are samples of keywords that summarize lots of meaning. Yet compression of human language leads to context and even meaning loss. Let’s talk about the KISS principle.

Down the memory lane

KISS stands for keep it simple stupid. It’s said that Kelly Johnson, a renowned aeronautical engineer from Lockheed, coined the term. Johnson is best known for his planes and jets key to the World War II Allies victory.

Kelly is also known for his management style that became norm in his lifelong company, Lockheed. He led an area that became known as Skunk Works, the cradle of all his brilliant creations. As per Ben Rich, author of Johnson’s posthumous biography:

a concentration of a few good people… applying the simplest, most straightforward methods possible to develop and produce new products

Ben Rich, describing the Skunk Works division

There’s also reports on how Johnson challenged his engineering team to design solutions that a regular engineer would be able to fix using common tools. This led to simple solutions that were stupid to fix. Hence the term. How does that relate to software engineering though?

Simplicity

Let’s elaborate on what simplicity means. The Merriam-Webster dictionary defines simplicity as:

  1. the state of being simple, uncomplicated, or uncompounded
  2. lack of subtlety or penetration
    1. INNOCENCE, NAIVETÉ
    2. FOLLY, SILLINESS
  3. freedom from pretense or guile : CANDOR
  4. directness of expression : CLARITY
  5. restraint in ornamentation : AUSTERITY

Out of all options above, the 4th and 5th describe better what the term conveys. We may say something is simple when its easy to explain (clarity) and devoid of distractions that’d hinder understanding (austerity).

Baseline

How simple can something be? I’m able to understand someone explain to me a concept about engineering in plain English. I’ll find it complex and fail miserably if they try to do the same explanation in say, French or German. That means simplicity relies on an implicit, common baseline shared between both ends.

Providing the same explanation by mimicking or drawing will certainly increase the complexity to convey the same message. And even those two methods also need their own baseline as well. What a thumbs up mean to one person may not mean the same to someone else.

Without that common baseline, the receiving end will struggle. I may be able to understand around 80% of Spanish due to is similarity to Portuguese. Yet some language constructs or specific words that I’m not familiar with can throw me off in a second.

Yet we don’t account for that complexity thanks to translation or by learning the idiom. Same goes for programming languages 😉

Perspective

Simplicity depends on the target audience and their point of view over the solution. Lets take for instance the Blackbirds as seen by:

📝 Note

Before I get cancelled: please, I hold no prejudice towards farmers. I don’t mean to say they’re ignorant or misinformed. My point is that the knowledge related to aircraft is specific so anyone outside that field will most certainly have no more knowledge than any other casual passer-by. Unless they’re enthusiasts about the topic.

Less is more - sometimes

A recurring interpretation among engineers is that KISS means doing less. Some even preach it should be the minimal necessary to make it work. While certainly one of Johnson’s tenets was to produce minimal designs, it had to solve the problem as best as they could at the time. It also had to be clear and easy for their engineers and mechanics to understand.

That didn’t hold back Johnson’s team to do one of the most complex projects the world ever saw that era. In the 1960’s they released the Blackbird, an aircraft that still holds world records to date. As per Johnson himself:

The idea of attaining and staying at Mach 3.2 (more than three times the speed of sound) over long flights was the toughest job the Skunk Works ever had and the most difficult of my career.

Aircraft operating at those speeds would require development of special fuels, structural materials, manufacturing tools and techniques, hydraulic fluid, fuel tank sealants, paints, plastics, wiring, and connecting plugs. Everything about the aircraft had to be invented.

Clarence "Kelly" Johnson, about the Blackbirds design

They had to invent everything from scratch. Isn’t that… non-KISS? Over-engineering even? I’m sure they could solve it with some extra glue and regular connecting plugs instead… 🌚

Doing less can harm the comprehension of the final design, let alone deliver the expected results. The Blackbirds are nothing short of an engineering wonder, yet they were in service for over 30 years for their quality, performance and easy maintenance. That last part is where Johnson applied KISS consistently: the product itself may be complex, yet its design, build, operations and maintenance are not.

The opposite certainly applies: imagine if the Blackbirds required all sorts of unique screwdrivers, hidden compartments and specific ways to access something. It’d be tiresome to go through that, as the nuances of it would slip the most attentive eye. The excessive level of “ornaments” distracts the user and may lead to a loss of context.

Even more so if they decided to add all sorts of extras to the end product. A small LCD so the pilot could watch their favorite series during a 1:54h trip from New York to London? Marvelous! What about a popcorn dispenser? Delicious.

Finding the sweet spot

Here’s my definition of the KISS principle, based on its origins: the art of designing a solution to a problem using a set of components and tools that leads to a clear, ease to explain, understand and maintain end product. That last part is key to debunk the association KISS = less.

💬 Short and sweet

Keep it simple to understand and stupid easy to maintain.

In coding terms, skipping to isolate a certain functionality does more harm than good. The fear of “increased complexity” or “excessive ornamentation” gives birth to functions and scripts with hundreds of lines of code and a high cyclomatic complexity.

To avoid the use of more capable mechanisms of a language or even a more capable language altogether leads to hard to understand and maintain code. Sure, breaking down functionality into smaller pieces will add extra code. Yet those that follow the domain-driven design principles have a clear intention. Its easy to understand a dozen clean components than a 200+ lines-long function.

Likewise, overly-short and cryptic mechanisms may look clever, yet they increase the cognitive load required to understand something. Certain languages like Haskell and Golang don’t even support ternary operators as they contribute to create complex expressions more often than not.

ℹ️ Info

Languages that do not have the ternary operator may still support a stricter version of an if/else construct as the right-hand side value.

For instance in Python and Haskell you can initialize values with an expression that must have both the if and else blocks returning a value.

Let’s do an exercise here with one of my favorite love-hate features of Bash: variable expansion. Do you know from the top of your head what’s happening on this script?

FOO=""

echo "${FOO-hello}"
echo "${FOO:-hello}"
echo "${FOO?hello}"
echo "${FOO:?hello}"
echo "${FOO+hello}"
echo "${FOO:+hello}"
echo "${FOO=hello}"
echo "${FOO:=hello}"

This code is short and works. Yet its hard to understand and explain, hence far from KISS. If you were able to guess with confidence and right what each line does then congratulations, you need some vacations. Urgently. Here’s the reference of how the Bash variable expansions above work.

ExpressionFOO="world"FOO=""unset FOO
${FOO:-hello}worldhellohello
${FOO-hello}world""hello
${FOO:=hello}worldFOO=helloFOO=hello
${FOO=hello}world""FOO=hello
${FOO:?hello}worlderror, exiterror, exit
${FOO?hello}world""error, exit
${FOO:+hello}hello""""
${FOO+hello}hellohello""

As readable as it can get. And I didn’t even touch the prefix, suffix and replacement expansions. Now let’s see how a dual of that feature would look in Golang:

//go:build ignore

package main

import (
	"fmt"
	"os"
)

func main() {
	os.Setenv("FOO", "")

	fmt.Println(getEnv("FOO", WithValueIfUnset("hello")))       // -
	fmt.Println(getEnv("FOO", WithValueIfEmpty("hello")))       // :-
	fmt.Println(getEnv("FOO", WithMustBeSet()))                 // ?
	fmt.Println(getEnv("FOO", WithMustBeNonEmpty()))            // :?
	fmt.Println(getEnv("FOO", WithValueIfSet("hello")))         // +
	fmt.Println(getEnv("FOO", WithValueIfNonEmpty("hello")))    // :+
	fmt.Println(getEnv("FOO", WithUpdateValueIfUnset("hello"))) // =
	fmt.Println(getEnv("FOO", WithUpdateValueIfEmpty("hello"))) // :=
}

func getEnv(key string, options ...EnvVarOptionFn) string {
	envCtx := envVarContext{
		key: key,
	}

	return envCtx.Get(options...)
}

// EnvVar provides a way to get environment variables with optional conditions
// and transformations, such as default values when the upstream is unset or
// empty. It also allows to update the value.
type EnvVar interface {
	// Get retrieves the environment variable value and returns a result based on
	// the options rules. Options run in a FIFO order
	Get(options ...EnvVarOptionFn) string

	// Set updates the environment variable, i.e. it changes the OS-provided
	// process memory area, so future calls to os.Getenv returns the new value
	Set(value string)
}

// EnvVarContext represents the state of an environment variable and allows
// overriding the local value e.g. to provide a default one.
type EnvVarContext interface {
	EnvVar

	// Key returns the environment variable name
	Key() string

	// Value returns the environment variable value
	Value() string

	// IsSet returns whether the environment variable is defined
	IsSet() bool

	// SetContextValue replaces the environment variable value within the context
	// only i.e. it does NOT change the OS-provided process memory area. Future
	// calls to os.Getenv returns the current value
	SetContextValue(value string)
}

type envVarContext struct {
	key   string
	value string
	isSet bool
}

func (envVarCtx *envVarContext) Key() string {
	return envVarCtx.key
}

func (envVarCtx *envVarContext) Value() string {
	return envVarCtx.value
}

func (envVarCtx *envVarContext) IsSet() bool {
	return envVarCtx.isSet
}

func (envVarCtx *envVarContext) Set(value string) {
	os.Setenv(envVarCtx.key, value)
	envVarCtx.isSet = true
	envVarCtx.value = value
}

func (envVarCtx *envVarContext) SetContextValue(value string) {
	envVarCtx.value = value
}

func (envVarCtx *envVarContext) Get(options ...EnvVarOptionFn) string {
	envVarCtx.value, envVarCtx.isSet = os.LookupEnv(envVarCtx.key)

	for _, option := range options {
		option(envVarCtx)
	}

	return envVarCtx.value
}

type EnvVarOptionFn func(envCtx EnvVarContext)

func WithValueIfUnset(value string) EnvVarOptionFn {
	return func(envVarCtx EnvVarContext) {
		if envVarCtx.IsSet() {
			return
		}

		envVarCtx.SetContextValue(value)
	}
}

func WithValueIfEmpty(value string) EnvVarOptionFn {
	return func(envVarCtx EnvVarContext) {
		if envVarCtx.Value() != "" {
			return
		}

		envVarCtx.SetContextValue(value)
	}
}

func WithMustBeSet() EnvVarOptionFn {
	return func(envVarCtx EnvVarContext) {
		if envVarCtx.IsSet() {
			return
		}

		panic(fmt.Errorf("environment variable %s must be set", envVarCtx.Key()))
	}
}

func WithMustBeNonEmpty() EnvVarOptionFn {
	return func(envVarCtx EnvVarContext) {
		if envVarCtx.Value() != "" {
			return
		}

		panic(fmt.Errorf("environment variable %s must not be empty", envVarCtx.Key()))
	}
}

func WithValueIfSet(value string) EnvVarOptionFn {
	return func(envVarCtx EnvVarContext) {
		if !envVarCtx.IsSet() {
			return
		}

		envVarCtx.SetContextValue(value)
	}
}

func WithValueIfNonEmpty(value string) EnvVarOptionFn {
	return func(envVarCtx EnvVarContext) {
		if !envVarCtx.IsSet() || envVarCtx.Value() == "" {
			return
		}

		envVarCtx.SetContextValue(value)
	}
}

func WithUpdateValueIfUnset(value string) EnvVarOptionFn {
	return func(envVarCtx EnvVarContext) {
		if envVarCtx.IsSet() {
			return
		}

		envVarCtx.Set(value)
	}
}

func WithUpdateValueIfEmpty(value string) EnvVarOptionFn {
	return func(envVarCtx EnvVarContext) {
		if envVarCtx.IsSet() && envVarCtx.Value() != "" {
			return
		}

		envVarCtx.Set(value)
	}
}
⚠️ Warning

Please do NOT use this code in real projects!

This is a silly example, done to port and keep parity with the Bash functionality. It lacks proper error handling and even uses the panic mechanism which is not recommended. There’s better ways to handle configuration and environment variables in languages such as Golang.

I personally recommend the spf13/cobra and its peer package spf13/viper to handle environment variables, command line flags and configuration files seamlessly.

I’ll grant you that’s way more code to get those quirk variable expansions done. Yet if you read the main function its much clearer what each line does. Even if you’re not familiar with Golang it should be faster to deduct what’s going to happen without deep-diving into its implementation. That’s KISS with a dash of DDD to finish it off.

📝 Note

My goal here isn’t to say Golang is better than Bash, nor that you should replace all your scripts with binaries. I used those languages as the latter has plenty of unreadable quirks and cryptic design choices, while the former is easier to produce a dual with a KISS design.

Over-engineering

Ah yes, the dreaded over-engineering label. Converting that Bash script to Golang is certainly an over-engineering, despite how KISS it may be due to the improved readability. Lets ignore the fact that such conversion happened. We’ll focus on the design of the environment variables expansion rules as a direct requirement of an existing Golang code base.

Some may consider the EnvVar and EnvVarContext interfaces “non-KISS” or an over-engineering as well. Sounds like You Ain’t Gonna Need It (YAGNI) instead of KISS, as the alternatives are arguably more complex:

Takeaway

Making your design easy to understand and maintain while delivering the expected result is the goal of the KISS principle. A design that focus on basic/quirky languages and primitive constructs is a distorted derivative of the Occam’s Razor principle, not KISS. Be careful, its sharp edges will hurt you at some point.