The KISS principle in coding for Swift developers.

Sayed Mahmudul Alam
8 min readJun 15, 2021

--

What is & whence KISS?

KISS (with all the letters in the capital) is an acronym for Keep it simple, stupid. Or formally, Keep It Simple and Straightforward [1]. It is a principle which states that a system should be designed in such a way that it’s easy to understand the internals later. As a result, making any changes will require minimum effort. It’s thought to be originated and pioneered by an aeronautical engineer named Kelly Johnson. While making a jet aircraft as a lead engineer, Kelly guided his designers to keep the system simple enough so that anyone with a primary mechanic’s training and basic tools can repair it in a combat situation [3]. If the system works very well but reconfiguring/repairing is complex, then the design doesn’t follow the KISS principle. The principle is relevant and useful to any field that designs a system. E.g., For aerospace engineers to design an aircraft system or a television manufacturer to make a remote controller and, of course, for software engineers to design software.

Purpose of the Article

Primarily, software engineers write codes, write unit/UI tests, decide architectures, and do many other things. As coding is the most fundamental in software engineering, this article only focuses on that. It tries to convey whether it is possible to replace hard-to-understand and verbose code with simpler and concise ones while keeping the same behavior. So that, it follows the KISS principle. The principle is explained by comparing pieces of code in five different scenarios. Though the codes are in Swift most of the underlying ideas could be applied to similar languages.

Motivation

Software development principles, e.g., SOLID, DRY, YAGNI, KISS, etc converge to a common purpose of writing healthy code that can be easily read, extend, and maintained. KISS explicitly encourages writing simple code. There are many pros to that. For example,

  • Making other's life easy: Writing code is a job, but writing good code is an act of responsibility. If someone writes simple code, then other fellow coders can easily read and understand that. It makes everyone’s life easy.
  • Self-love: Often, developers need to pull the back gear, and have to look into their code to fix bugs or add new features, etc. For any such cases, developers will struggle to understand their own code if written impulsively and in a tangled way. So, writing granular and straightforward code is a charity to self.
  • Better Code: A simple code is not necessarily a good code, but a characteristic of a good code. If it is possible to add simplicity, then one step is already made toward a better code. According to Occam’s Razor, “The simplest explanation for some phenomenon is more likely to be accurate than more complicated explanations.”[4]. Keeping that in mind, if there are multiple ways to write a piece of code, the simplest one will likely be accurate. It might seem vague to connect programming with something like Occam’s Razor, which has deep roots in literature and philosophy, but some studies already have tried to mitigate the gap. [5][6][7].
  • Better Coder: Though it is controversial, some people think, Albert Einstein once said, “If you can’t explain it to a six-year-old, you don’t understand it yourself.” Whether he said it or not, the idea makes sense. The ability to express something plainly and simply reflects one’s own firm grasp over it. So, the more expressively one can code, the more proficient s/he is. Such proficiency can be achieved by practice, experience, and most importantly, having the willingness.
  • Less Error-Prone Code: A little opinionated, but sometimes developers get inclined to add unnecessary abstraction without forethought. Perhaps, to make the code more reusable and modular? To achieve that, some extra layers of code add up. The purpose of these codes is merely to cater to the abstraction. But the actual code having the business logic is left deep inside a hazy facade. After few days, a new requirement comes, and now the old abstraction doesn’t comply without making some alterations. These alterations introduce new codes that make the abstraction comply with the new requirement and also make sure existing requirements don’t break. Oftentimes these codes are conditional statements, and if not handled carefully, they might include buggy edge cases. Simple code is not in opposition to abstraction. However, the abstraction has to be purposeful and adoptive. So that, in the future, they will not become vulnerable to errors.

“Talk is cheap, show me the (Simple) code.”

Now, let’s go through some approaches and see whether simple coding can improve readability and extendability.

Shorthands

One way to keep the codebase less cluttered is to use shorthands where possible. One such shorthand is the ternary operator(?:). It is very elegant, however, often neglected. Let’s consider the below code.

For the code above, suppose that a boolean response flag has been received from the back-end server and stored in the isSuccess variable. Based on the value, the text color of a label is being updated, and afterward, it will be used to show some message to the users. Here, the if-else statement has been used.

The statement can be written using a ternary operator like below, and the output will be the same.

The latter one is small and easy to read. Instead of four, it’s just a single line of code. Although it is debatable whether fewer lines mean better code, in this case, it is lite than an if-else statement. Though one caveat is not to use nested ternary operators as it might result in hard-to-read code.

Some other shorthands: Nil coalescing, Shorthand parameter syntax, Add then assign to (+=), etc.

Adequate Alternatives

One thing to remember when following the KISS principle is not to break a butterfly on a wheel. As an example, when defining a method, one can think about the computed property first. If the computed property is sufficed then there is no need to use a method. In the code below, there is a method getAgeAfterFiveYears() which returns a person’s age that they will have after five years.

The method can be replaced by a computed property. Computed properties are a special kind of property that doesn’t store value. Instead, they calculate a value based on other properties and return that. [8]

Now both the method and computed property return the same value. So, they have the same behavior. But to the caller, the computed property is just another property, not a method.

Long story short, if a method is very simple, e.g., it doesn’t accept any arguments, doesn’t do any heavy work, etc then probably it’s not a method, but a good candidate for a computed property. Here, a computed property is adequate, and using it not only follows the KISS principle but follows the so-called Swifty way of writing code as well. It is always wise to look for alternative ways that are sufficient.

NOTE: It might be unclear why getAgeAfterFiveYears() and ageAfterFiveYears don’t have the return keywords. It’s because they are using something called Implicit Return. [9]

Syntactic Sugars

Programming languages already offer some syntactic sugars to the developers so that they can avoid writing verbose code. Let’s recall the Person struct defined in earlier examples. Suppose there is an array called people that holds objects of Person. Now the objective is to find out the index of the item from that array that has the name property equals “Chloé Zhao”. Below is one way to do it.

An alternative way to achieve the same outcome is to use firstIndex(where:) method of the Array collection type, offered by the Swift language itself.

Some of the Syntactic Sugars offered by Swift: Trailing closures, Property Wrappers, if-let, etc.

NOTE: Before moving forth, in the first example, a for-loop has been used for which the time complexity is O(n). This is also true for the second example [10]. Under the hood, firstIndex(where:) does the same (or similar) operation. So the last example is preferable for the sake of obtaining simplicity. It will not improve the time complexity.

The Higher-Order Functions

Higher-order functions are functions that take one or more functions as arguments or/and return a function as its result. [11]. They are things from the Functional Programming(FP) universe. Some languages are symbiotic as they have elements that co-exist which are borrowed from other kinds of languages. Fortunately, Swift is one of them as its collection types support some higher-order functions. E.g., Sorted, Map, FlatMap, Reduce, etc. In cases, higher-order functions can add simplicity and conciseness in code. Suppose an array of names needs to be populated from the people array mentioned in earlier examples. The most probable approach one would take by instinct would be,

An FP-ish way to do the same is,

It may appear as syntactic sugar. But it’s a whole different thing. Higher-order functions are much more flexible in nature. For example, Map applies a common operation to each element of a collection. The operation is provided by a closure as an argument.

The raywenderlich.com has a good article with examples about the FP and higher-order functions by Warren Burton.[12]

Coherence

In cases, it is advantageous to sacrifice some simplicity to gain more extendable code. It is an opportunity cost paid to achieve a positive coherence throughout the codebase. Let’s jump-start with examples.

The above code, based on the difficulty constant, prints some information about towers. Imagine the towers are from the video game Mortal Kombat 3. Obviously, the if-else doesn’t look concise, and string matching is not safe. It is also not future-proof because if there are new difficulty value chimes in, the compiler will not be able to tell that new else-if blocks need to be added. The coder has to be mindful to add those themselves.

Fortunately, with the help of Enum (with raw values), Typealias, Tuples, and computed property, all the mentioned problems can be solved.

Now whenever a new difficulty type comes up e.g., Legendary, just a new case has to be added on Difficulty enum. The compiler will throw an error, and until that case is taken care of in the switch statement, the code will not run. So, the coder can’t leave out a case unattended by mistake.

Instead of the typealias, a struct could have been used, but there is no solid reason. Because most likely, the struct would not be used in other places, as it is trivial in nature.

NOTE: Apparently, the second approach is more verbose than the first one. Moreover, to the developers who are new to Swift, the code may seem alienating. But despite these, the second approach is easily extendable and safer. Maybe the syntax is not but, the characteristics and usability are simpler. Though it sounds like an oxymoron, sometimes simplicity can be achieved through some amount of controlled complication, a balance between over-engineering and under-engineering.

Last Words

Thanks to all who took the time to read through. I hope it was somewhat informative. Hands down, writing good code is not easy, and I believe it is a career-long journey, and an article or a book alone is not sufficient. I am on the journey, and I have tried to put down some of the useful tips I have learned so far. I also would like to make an honest confession. In places, I might have sounded a little emphatic. Because it is hard, providing references to statements that are learned from experience but not from citable learning materials. By and large, try to follow KISS, write easy-to-read code, don’t over-engineer. And, we the Swift developers, let’s propose a new phrase for KISS. “Keep it simple and Swifty” :)

Long live Bangladesh 🇧🇩

--

--