1 Sep 20267 min read

Three Laws of Readable Code

Great code is easy to read. Deep nesting makes it hard.

func main() {
    if !isMaintenancePeriod {
        if isAuthenticatedUser {
            if isAuthorizedUser {
                for product in cart {
                    switch product.type {
                        case .alcohol:
                            total += alcoholTax
                        case .electronics:
                            total += electronicsTax
                        default:
                            total += generalTax
                    }
                }
            } else {
                fatalError("user not authorized")
            }
        } else {
            fatalError("invalid user credentials")
        }
    } else {
        fatalError("feature unavailable")
    }
}

Every level of nesting is one more condition to remember. By the time you reach the core logic, you’re holding all of them in your head, and you still have to understand the logic itself.

That’s why the first law is simple: keep it flat.

Law 1 – Keep It Flat

Inversion

Start with inversion. Handle the failure first, then exit early. In Swift, that’s what guard is for.

Instead of wrapping the core logic in a condition, with the failure handled in an else:

State the requirement first. If it isn’t met, nothing else runs.

That removes one level of nesting.

Apply the same pattern to the other conditions.

Now no one has to hold these conditions in their head. If the code reaches a given line, every check above it has passed. You can forget them and focus on what comes next.

Merge

Next, merge related checks. isAuthenticatedUser and isAuthorizedUser both ask whether this user is allowed in, so they can share one guard.

The trade-off is a less specific error message: both failures now report the same thing.

Extraction

Finally, use extraction to move complex logic into functions of its own.

Start with the merged guard. At a glance, it doesn’t say what the two conditions are for. Good code always considers the next person to read it, so move them into a function with a descriptive name.

The function returns whether both conditions are true, so the guard now reads as a single question: is this a valid user?

Next, move the entire switch into a function of its own.

Now main tells the whole story at a glance: if it isn’t a maintenance period and the user is valid, calculate the taxes for each product in the cart.

That story was always there. It was just hard to see:

Law 2 – Write It Once

The second law is about how you organize your code. Here are two functions from the same program.

func getUser(request: Request, response: Response) throws {
    let userID = String(request.path.dropFirst("/user/".count))

    let cachedUser = cache.withLock { $0[userID] }

    let user: User
    if let cachedUser {
        user = cachedUser
    } else {
        let query = "SELECT user_name, email FROM users WHERE user_id = ?"
        let statement = try database.prepare(query)
        let row = try statement.queryRow(userID)
        let fetchedUser = User(username: row["user_name"], email: row["email"])

        cache.withLock { $0[userID] = fetchedUser }

        user = fetchedUser
    }

    response.headers["Content-Type"] = "application/json"
    response.body = try JSONEncoder().encode(user)
}
func getUsers(request: Request, response: Response) throws {
    let requestBody = try JSONDecoder().decode(RequestBody.self, from: request.body)
    let userIDs = requestBody.userIDs
    var users: [User] = []

    for userID in userIDs {
        let cachedUser = cache.withLock { $0[userID] }

        let user: User
        if let cachedUser {
            user = cachedUser
        } else {
            let query = "SELECT user_name, email FROM users WHERE user_id = ?"
            let statement = try database.prepare(query)
            let row = try statement.queryRow(userID)
            let fetchedUser = User(username: row["user_name"], email: row["email"])

            cache.withLock { $0[userID] = fetchedUser }

            user = fetchedUser
        }

        users.append(user)
    }

    response.headers["Content-Type"] = "application/json"
    response.body = try JSONEncoder().encode(users)
}

Each one is simple on its own. The trouble starts when something needs to change.

Say you need to remove caching from the app. The same logic lives in multiple places, so you have to search everywhere, and you could miss one.

That’s the cost of duplication, and it’s why the second law is to write it once.

Move the shared logic into a single function that both callers can use.

func getSingleUser(userID: String) throws -> User {
    let cachedUser = cache.withLock { $0[userID] }

    let user: User
    if let cachedUser {
        user = cachedUser
    } else {
        let query = "SELECT user_name, email FROM users WHERE user_id = ?"
        let statement = try database.prepare(query)
        let row = try statement.queryRow(userID)
        let fetchedUser = User(username: row["user_name"], email: row["email"])

        cache.withLock { $0[userID] = fetchedUser }

        user = fetchedUser
    }

    return user
}
func getUser(request: Request, response: Response) throws {
    let userID = String(request.path.dropFirst("/user/".count))
    let user = try getSingleUser(userID: userID)

    response.headers["Content-Type"] = "application/json"
    response.body = try JSONEncoder().encode(user)
}
func getUsers(request: Request, response: Response) throws {
    let requestBody = try JSONDecoder().decode(RequestBody.self, from: request.body)
    let userIDs = requestBody.userIDs
    var users: [User] = []

    for userID in userIDs {
        let user = try getSingleUser(userID: userID)
        users.append(user)
    }

    response.headers["Content-Type"] = "application/json"
    response.body = try JSONEncoder().encode(users)
}

The code that writes the response to the client is duplicated too, so give it the same treatment.

func writeResponse(_ body: some Encodable, to response: Response) throws {
    response.headers["Content-Type"] = "application/json"
    response.body = try JSONEncoder().encode(body)
}
func getUser(request: Request, response: Response) throws {
    let userID = String(request.path.dropFirst("/user/".count))
    let user = try getSingleUser(userID: userID)

    try writeResponse(user, to: response)
}
func getUsers(request: Request, response: Response) throws {
    let requestBody = try JSONDecoder().decode(RequestBody.self, from: request.body)
    let userIDs = requestBody.userIDs
    var users: [User] = []

    for userID in userIDs {
        let user = try getSingleUser(userID: userID)
        users.append(user)
    }

    try writeResponse(users, to: response)
}

Now a change happens in one place. The code is simpler, too, and easier to read.

Law 3 – Name It Clearly

What does this code do?

The names don’t say. That brings us to the third law: name it clearly.

There are many naming conventions, and any of them is a good start. What matters is choosing names that mean something to the next person who reads your code. As the Swift API Design Guidelines put it: “Clarity at the point of use is your most important goal.”

Rename a few things, and the code explains itself.

Keep it flat. Write it once. Name it clearly.