Celebrating 10 Years!

profile picture

More Types

October 22, 2018 - Roundwall Software

Sometimes the answer to your programming problem is "use more types". I spoke at Mobilization 8 this past weekend and I discussed a particular programming issue with someone. Here is a version of that problem (with the details changed to protect the innocent).

The Setup

Let's say you have a struct like so:

struct CreditCard {
  let number: String
  let expiration: Date
}

Now let's also say that whenever you display the number in your app, you need to mask part of the number. Rather than display 4242 4242 4242 4242 on the screen, you want to only display xxxx xxxx xxxx 4242. The person I spoke with had a project where they accomplished this goal by adding to their general CreditCardUtils struct.

struct CreditCardUtils {
/// various other methods here, at least 100 lines of code, some that depend on system objects.

  static func mask(number: String) -> String {
  }
}

extension CreditCard {
  var maskedNumber: String {
    return CreditCardUtils.mask(number: number)
  }
}

This way technically worked. Any time you needed to display the number on the screen, you could ask a card for the masked version. However, when he tried to write tests for it, he ran into a problem. This masking method was a static function of a struct which could not be created without access to system objects. Even if you did go through the effort to create one, it did not matter because you could not replace the call with your fake in a CreditCard.

After some discussion, here's what I came up with:

The Fix

A New Type

Replace the super-generic type String with a new custom type CreditCardNumber there are two ways to do that depending on what else you need to do.

struct CreditCardNumber {
  let number: String
}

/// or

typealias CreditCardNumber = String

Move The Behavior To The Type

extension CreditCardNumber {
  var masked: String {
  }
}

Rather than depending on some generic Util type to do the work, we move that behavior onto the object that has this behavior. A card number can create a masked representation. This is easily testable and no longer requires elaborate gymnastics in your tests. Make a card number with a value, verify the masked version it generates looks correct, move on to your next task. This does have the consequence of making your code a bit more verbose though:

let card = CreditCard(number: CreditCardNumber(number: "4242 4242 4242 4242"), expiry: Date())

Slightly more messy looking, but if you really want to fix that you can! If you make the new type conform to ExpressibleByStringLiteral, you can create cards in much the same way as you would before.

struct CreditCardNumber: ExpressibleByStringLiteral {
    typealias StringLiteralType = String
    
    let rawValue: String
    
    init(stringLiteral: String) {
        self.rawValue = stringLiteral
    }
}

let number: CreditCardNumber = "4242 4242 4242 4242"

Now that you can create card numbers with string literals, creating new cards looks like it did originally:

let card = CreditCard(number: "4242 4242 4242 4242", expiry: Date())

I don't personally think this step is necessary, but I've seen projects where they avoid creating the necessary types to represent their information nicely because their inits look messier. If you're one of those people, this trick can help.

Use A Formatter

After thinking about this some more, I realized that maybe this masked value should not be computed by the CreditCardNumber. We are trying to format data for display on the screen, so we should use a formatter, just like we would if we wished to display dates, currencies, or weights.

class CreditCardNumberFormatter: Formatter {
}

A formatter is a fairly small object with no dependencies so it is still easy to test. Using a Formatter subclass also allows you to use the same logic in your Mac app with Cocoa Bindings and such. Generally any time you need to format your data to display on the screen, there's likely an associated formatter. If one does not exist, like in this example with credit card numbers, perhaps you should make one. There are quite a few already:

These formatters consider units, different use cases, as well as language. All testable in a fairly neat package.

Fin

So now maybe you're armed with a bit more knowledge and you can go forth and test your things. Enjoy!