github twitter instagram email rss
Random advice for past me
Jul 30, 2018
6 minutes read

The end of this month will conclude my second year of full-time iOS development. I started right after graduating from university, where I stuffed myself with software theory for three years. After all the patterns, theorems and best practices I was ready to apply my knowledge and conquer the realm of software development.

Two years later, I’ve written and reviewed thousands of lines of code, shipped dozens of features, fixed hundreds of bugs and learnt very, very much. From my experience, the theory I gathered from university was extremely valuable, but at least equally important was learning how to apply it.

In this blog post, I’ll try to summarise some of the things that came to mind when I asked myself “What did I learn?”

Pragmatism vs. dogmatism

I think most of the times, constructing a software project looks somewhat like:

  • look at the requirements
  • determine a fitting architecture
  • build it.

That’s it.

Except, that’s not how products evolve. Requirements change. New, unexpected features require your codebase to adapt. Inevitably, a point will come in which a fundamental aspect of your project would need to be re-built in order to keep its pristine, best-practice nature.

And maybe, in your project you are able to do that. But the more limited your resources are (and the less a customer can empathise with struggling coders), the less refactoring is going to be paid for. As much as it can hurt, I’ve learnt that one needs to come to terms with the fact that you can not always write beautiful code. Healthy pragmatism, when applied carefully, can go long ways in shipping features.

Naturally, after realising this, you try to adapt and prepare yourself for future, unforeseen scenarios. This however has the potential of leading you down another troublesome path..

YAGNI, YAGNI, YAGNI

“You ain’t gonna need it” is a fantastically memorable acronym for the habit of writing code you’re never going to need.

I’ve experienced countless times, that in the habit of trying to over-prepare for possible future scenarios, I’ve spent a large amount of extra time generalising my already finished components into reusability. Maintaining and testing these can cost a multitude of what you originally spent developing the feature and might never be used in the future.

Optimise afterwards, not beforehand.

This sounds plausible, but can be much harder to stick to than expected. We all get taught to code DRY, to avoid writing the same code twice. And once you know how to extract calls into methods, methods into classes, and so on - you see opportunities to optimize everywhere.

My very first macOS application had 12 different coloured buttons. I didn’t know how to write parametrised initialisers in Objective-C yet, so every button had its own class.

Barbaric! You would immediately refactor BlueButton, RedButton, GreenButton into Button(color:).

But should you?

Granted, my buttons are a simple example, I would probably refactor them too. But as things get more complex, I claim we tend to over-engineer code because under no circumstances can we ever write the same code twice!

You’re building a car, but maybe at some day it also needs to fly, swim and dive. They all need an engine, and I wouldn’t want to build that twice, so I’m building a GenericUniversalExtensionEngine.

A more calculated approach would be to build a car and move on. Later, after we’ve built a plane, a boat and a maybe even a submarine we can think about how and if we could extract reusable components and generalise our engine.

Maybe DRY should be DRYT, Don’t Repeat Yourself Twice. I’m not proposing we stop watching out for code repetition, but I think it’s important to find a balance between calculated refactoring and pointless over-engineering.

KISS me

We’ve all been taught to keep our code “stupid simple”, but I think programmers have a tendency to turn this into “super short”.

Trying to make your code as concise as possible can turn it into the exact opposite of simple. Swift for instance provides some super cool tools to keep yourself short, a clever combination of filter, sort, compactMap or reduce can replace countless for-loops.

But if you’re not careful, you’re trading that for a big loss in readability for both fellow coders and your future self. Over time, I found that on this matter, subtle adjustments can go great lengths.

Here’s an example of some code I’ve written a while back:

let productCategorySorting = salesInfo.categories?
 .filter({ Int($0.key) != nil }) // Only sort with valid index
 .map({ (Int($0.key)!, $0.value) }) // map dict to pairs
 .sorted(by: { $0.0 < $1.0 }) // sort based on index, empty if nil
 .map({ $0.1 }) ?? [] // only store the values

This code is relatively short, a non-functional version would likely be much longer. I recently refactored this code, because even though there are comments explaining what’s going on, it took me much longer to comprehend and evaluate than it should have.

Here’s the refactored version:

public extension SalesInfo {
  public var sortedProductCategories: [String] {
    guard let categories = categories else { return [] }
    let validCategories: [String: String] = categories.filter({ category in
        return Int(category.key) != nil
    })
    let indexedCategories: [(Int, String)] = validCategories.map({ category in
        return (Int(category.key)!, category.value)
    })
    let sortedCategories: [(Int, String)] = indexedCategories.sorted(by: { $0.0 < $1.0 }) 
    let values: [String] = sortedCategories.map({ $0.1 })
    return values
  }
}

I’m still using the short and convenient filter, map and sort-Methods, but each step is broken down and its result stored in to a constant.

What I’ve tried to do is provide “mental save states” throughout the function. Each step can be understood and evaluated separately, and if the result is used later, one doesn’t need to remember every implementation detail of each step.

Inherently, I could drop the explanation comments, since the constant names document my intention by themselves. In a similar fashion I’d even encourage writing functions which may only ever be called once, but which capsule logic for local evaluation, and provide a description of intent simply by their method name.

Adjustments like these reduce the cognitive load while reading code. You have to keep less things stored in your mental cache and can focus on grasping the core intention of what’s going on.

Path to experience

The last aspect I’d like to mention is less of a learnt lesson and more of an encouraging observation. When I started developing my first features on meaningful projects, I felt a bit overwhelmed. Without much experience in the iOS ecosystem, my efforts were constrained to finding any possible solution to a problem. Especially when it comes to code architecture, knowing how and why to structure code in certain ways, changed significantly over time.

My perspective and attitude adjusted from..

“I know how to do this in any way”

to…

“I know how to do this in multiple ways”

.. and more importantly..

“I know how to do this the wrong way

.. which finally lead me to points in which I can confidently say

“I know a right way to do this”

Getting to this point feels incredibly rewarding and inspiring to me. Looking at code and thinking of alternative approaches, pondering about their validity and effectiveness is what brings me joy in this field and is the reason that after two years, I’m nothing but excited to dig deeper and write 10 more blog posts like this with many many more things to learn. 🙂

Thanks for reading and have a great day!


Back to posts