UPGRADE YOUR SKILLS: Learn advanced Swift and SwiftUI on Hacking with Swift+! >>

Improving your Swift code using value objects

They’re like super-charged value types.

Paul Hudson       @twostraws

What if I told you that you can write Swift code that’s easier to understand, easier to test, has fewer bugs, and is more secure, all with one simple change?

No, this isn’t a scam, and in fact it’s using a time-tested technique called value objects. These have some similarities with value types but aren’t the same thing as you’ll see. Even better, you’ve already used at least one common type of value object, so hopefully the advantages will be clear pretty quickly.

Hacking with Swift is sponsored by RevenueCat

SPONSORED Take the pain out of configuring and testing your paywalls. RevenueCat's Paywalls allow you to remotely configure your entire paywall view without any code changes or app updates.

Learn more here

Sponsor Hacking with Swift and reach the world's largest Swift community!

What is a username?

Take a look at the following example Swift code and see if you spot a commonality:

func authenticate(user: String) { }
func purchase(product: String, quantity: Int) { }
func sendEmail(to recipients: [String]) { }

Yes, one obvious commonality is that all three are functions without any code inside, but there’s something else: all three use primitive types for their parameters.

Primitive types are things like strings, integers, and Booleans. In Swift they are all value types, which means they always have one unique owner rather than being shared, which itself is a great way to reduce complexity in our code. However, does an integer accurately represent the number of products in a purchase? Does a string accurately represent usernames?

The answer is almost certainly no. There’s a good joke by Bill Sempf that goes: “A QA Engineer walks into a bar. Orders a beer. Orders 0 beers. Orders 999999999 beers. Orders a lizard. Orders -1 beers. Orders a sfdeljknesv.” The point here is that quantities of a product need to fall within a certain range:

  • Negative values don’t make sense.
  • A zero quantity isn’t a purchase.
  • Very large quantities are likely to be invalid – unless you’re a store that sells individual peas!

As for usernames, you can imagine how the start of the authenticate(user:) method might look:

func authenticate(user: String) {
    // make sure usernames are at least three characters
    guard user.trimmingCharacters(in: .whitespacesAndNewlines).count >= 3 else {
        print("Username \(user) is too short.")
        return
    }

    // make sure usernames contain no invalid characters
    let illegalCharacters = ["@", "-", "&", "."]
    guard illegalCharacters.contains(where: user.contains) == false else {
        print("Username \(user) contains illegal characters.")
        return
    }

    // Proceed with authentication…
}

I don’t know how many hoops you might need to jump through in order to validate usernames in your company, but you will need some – even if it’s just to make sure the string isn’t empty once you’ve removed whitespace. You’ll also need to duplicate that logic in every method where usernames are accepted, because otherwise your code could end up in an invalid state.

Now, you might try to be smart and move such validation into an extension like this:

extension String {
    func isValidUsername() -> Bool {
        guard self.trimmingCharacters(in: .whitespacesAndNewlines).count >= 3 else {
            return false
        }

        let illegalCharacters = ["@", "-", "&", "."]
        guard illegalCharacters.contains(where: self.contains) == false else {
            return false
        }

        return true
    }
}

That certainly removes most of the duplication, but now you need to remember to call isValidUsername() everywhere username strings come in – and any place you don’t is potentially insecure or buggy.

A solution from domain-driven design

The solution here is to use a technique from domain-driven design (DDD) called value objects. It’s far from a new technique, but it’s resurfaced in my head because I got to attend a talk by Daniel Sawano – who, by the way, has a whole book on writing code that’s secure by design.

Value objects are custom types that are small, precise types that represent one piece of our application. Like value types value objects should be both immutable and equatable, but they also add in validation as part of their creation. This means that if you’re handed a value object you know for sure it’s already passed validation – you don’t need to re-validate it, because if it were invalid then it couldn’t exist.

We could update our username example to be a value object like this:

struct User: Equatable {
    let value: String

    init?(string: String) {
        guard string.trimmingCharacters(in: .whitespacesAndNewlines).count >= 3 else {
            return nil
        }

        let illegalCharacters = ["@", "-", "&", "."]
        guard illegalCharacters.contains(where: string.contains) == false else {
            return nil
        }

        self.value = string
    }
}

That code uses a failable initializer to ensure that it’s impossible to create an instance of User unless it has passed our checks. This is important, because it makes illegal states unrepresentable: the very existence of a User in our code means that it must be valid.

With that in place we can rewrite the authenticate(user:) method to something that’s more than a bit shorter:

func authenticate(user: User) {
    // Proceed with authentication…
}

Yes, that literally has no code now – there’s no need to validate any part of the username because Swift has already done it for us.

The benefit of this approach is something you’re almost certainly familiar with when it comes to URLs. Swift has a dedicated URL type that is used with things like Data(contentsOf:). Sure, loading data from a URL might fail, but it won’t be because you used it with the URL “This ain’t it chief” – using URL(string: "This ain't it chief") will return nil because that’s not a valid URL.

Not only do value objects lower the cognitive load by making our code simpler and safer, they also give us the flexibility to expand in the future – perhaps a User is just a username string right now, but in the future you might expand it to include email addresses, passwords, and more. With the User value object in place we can expand as much as we need to, but if we had just used a string then switching to a complex type later could mean a lot of refactoring.

Value objects vs over-engineering

There are two common ways folks push back when presented with value objects, and I’d like to address them head on.

First, it might seem like over-engineering to have lots of simple, dedicated types that could just be regular strings. This is a particularly common response from developers used to working on Apple platforms, because we are so accustomed to stringly typed APIs – i.e., APIs that use strings for their input, such as dequeuing table view cells, instantiating view controllers, and loading images.

Whether or not it’s over-engineering really depends on the problem you’re solving. I know some folks advocate using value objects everywhere, but in practice I can see why you might prefer not to if you have a value that’s being used in only one place.

While one article is unlikely to convince you to adopt value objects everywhere, I hope it will at least be enough to break you of so-called primitive obsession – a term value object advocates use to mean consistently choosing primitive types like strings and integers to represent data in your code.

The second common objection is that we can get some of the benefit of value objects using a typealias. For example, we could write code like this:

typealias User = String

func authenticate(user: User) { … }

That makes it clear to folks calling authenticate(user:) that we expect a username to be passed in, while also giving us the ability to change User to a different type in the future without having to rewrite so much.

However, the problem with typealiases is that they don’t allow the Swift compiler to help enforce our rules. In this instance, User is identical to a String in the compiler’s eyes, so you can pass in regular strings just fine.

More importantly, this approach doesn’t give us the most important of value objects: the ability to know for sure that ans instance of a value object has already been validated, and is safe to use without revalidation.

Smoothing the edges

One of the speed bumps introduced with value objects is that they can feel a little clumsy to create, particularly when you have to make a lot of them in your unit tests. At the very least you’re looking at code like this:

if let username = User(string: "paul") {
    authenticate(user: username)
}

However, Swift lets us do better than that thanks to the ExpressibleByStringLiteral protocol, which lets us create instances of a type from a plain string. For example:

struct User: Equatable, ExpressibleByStringLiteral {
    let value: String

    init?(string: String) {
        // same code as before
    }

    init(stringLiteral value: StringLiteralType) {
        self.value = value
    }
}

That allows us to call authenticate(user:) directly using a string, like this:

authenticate(user: "Paul")

Behind the scenes, that code is effectively the same as this:

authenticate(user: User(string: "Paul"))

While that’s more convenient, it has also completely bypassed the validation checks that are part of the failable initializer. However, in this instance it’s probably not as bad as you think because it only works with string literals (strings you’ve typed in by hand) which means either you’ve typed the string correctly or you haven’t.

This is a special class of error called a logic error: it’s not something that happens because your runtime state isn’t what you expected, it’s that your code is simply wrong, just like if you try to dequeue a table view cell using the wrong identifier.

There are two alternatives that are worth considering. First, you can forward your string literal initializer onto the regular initializer, like this:

init(stringLiteral value: StringLiteralType) {
    if let newUser = User(string: value) {
        self = newUser
    } else {
        fatalError("Invalid user: \(value)")
    }
}

As you can see that requires a fatalError() call in situations where we try to create a user from an invalid value, which might result in your code crashing. However, at least now your checks will still get run, forcing you to make sure all your hand-typed usernames are valid.

If you don’t want to introduce that fatalError() call into your production code, a second option is to leave ExpressibleByStringLiteral out of your primary type declaration and instead add it as an extension in your unit test target, like this:

extension User: ExpressibleByStringLiteral {
    public init(stringLiteral value: StringLiteralType) {
        if let newValue = User(string: value) {
            self = newValue
        } else {
            fatalError("Invalid user: \(value)")
        }
    }
}

This at least makes it easier to write user creation tests, without running the risk of surprise crashes in production.

Where now?

As you’ve seen, value objects help us write simpler, safer code by taking the immutability and natural equatability of value types and adding the ability to self-validate – I hope you give them a try!

If you’d like to learn more, I have some resources that are likely to help, although I’m afraid that none use Swift:

And if you’ve tried value objects in your own code, let me know how you got on – I’m @twostraws on Twitter.

Hacking with Swift is sponsored by RevenueCat

SPONSORED Take the pain out of configuring and testing your paywalls. RevenueCat's Paywalls allow you to remotely configure your entire paywall view without any code changes or app updates.

Learn more here

Sponsor Hacking with Swift and reach the world's largest Swift community!

BUY OUR BOOKS
Buy Pro Swift Buy Pro SwiftUI Buy Swift Design Patterns Buy Testing Swift Buy Hacking with iOS Buy Swift Coding Challenges Buy Swift on Sundays Volume One Buy Server-Side Swift Buy Advanced iOS Volume One Buy Advanced iOS Volume Two Buy Advanced iOS Volume Three Buy Hacking with watchOS Buy Hacking with tvOS Buy Hacking with macOS Buy Dive Into SpriteKit Buy Swift in Sixty Seconds Buy Objective-C for Swift Developers Buy Beyond Code

Was this page useful? Let us know!

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.