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’s 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:

  • Pilots: its simple to handle as they already know other aircraft and the controls are at the same position.
  • Mechanic engineers: simple to fix as the tools required are commonplace and its intuitive to access certain parts of the engine that often require repair.
  • Aeronautical engineers: simple design considering their domain knowledge, good schematics and well-written documentation.
  • Farmers: an absolute black box, the utmost complexity they’ve ever seen. No idea about how it works or any of that Mach talk.
edit-2Note

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 me 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 “optionals” 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.

message-circleShort 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.

infoInfo

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?

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
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:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
//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)
	}
}
alert-triangleWarning

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.

edit-2Note

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:

  • Standalone functions for each use-case. Lengthy function names and shared intent. Increased maintenance as functions share part of the logic plus a side-effect. By separating getEnv from each condition logic we apply both the Don’t Repeat Yourself (DRY) and KISS principles, making it easier to understand and maintain each part alone.

  • inline all the logic directly in the main body. Plain old spaghetti with all sorts of logic mixed up in a high cyclomatic complexity function. Also low to no reusability.

  • write custom operators like Bash. This is possible in functional languages that allow you to create operators such as :?. Haskell is one of those. Yet these operators would be specific to a single use case. There’s better ways to functionally represent the conditionals from our example with existing mechanisms of the language.

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.