Self validating models

Tue, 5 Jan 2021 00:00:00 +0000

Let's see how we can avoid building non valid objects that would break our app by using self validation when dealing with complex objects like phone numbers or IBAN.

Sometimes in your project you might have objects requiring some validation prior being considered as valid such as IBAN or phone number. In the later we often need to ensure user input is a valid phone number. Failing in doing so (or not doing it) could make your app at best behaving strangely and at worst crashing.

We could at first just work with a simple String:

let iban: String = "Not valid"

However by doing so our IBAN might be (and is) invalid. We would also have to check it is actually correct every time we use it. Not doing so could make the app at best behaving strangely and at worst crashing.

To avoid such pitfalls we can use self validating models.

Building a self validating model

First thing to do is to create an IBAN model. At first we'll consider everything data is valid:

struct Iban {
  let value: String

  init(_ string: String) throws {
    self.value = string
  }
}

Using throws we indicate that init might fail if input is invalid 💪. It now require us to be careful when trying to get a Iban instance:

do {
  let iban = try Iban("Not a valid IBAN")
}
catch {

}

We can't now work or ask an invalid Iban in our app without knowing about it 🎉. Well unless you have bugs in your algorithm that is 🤷‍♂️

Testing

One great thing about self validating objects is how your tests become more explicit. Let's write some to see:

class IbanTests: XCTestCase {

  func test__init__withValidIBANFormat__ItReturnAnIban() {
    let string = "FR1420041010050500013M02606"

    XCTAssertNoThrows(try Iban(string))
  }

  func test__init__lengthIsTooShort__IThrow() {
    let string = "FR142"

    XCTAssertThrows(try Iban(string))
  }
}

Right now our second test will fail so let's just implement our algorithm:

struct Iban {
  let value: String

  init(_ string: String) throws {
    self.value = try Self.validating(string)
  }
}

extension Iban {
  private static func validating(_ string: String) throws -> String {
    // take a look at https://en.wikipedia.org/wiki/International_Bank_Account_Number
    // if you ever wanted to implement a real IBAN algorithm
    let ibanLength = 23

    guard string.length == ibanLength else {
      throw IbanParseError.invalidLength
    }

    return string
  }
}

enum IbanParseError: Error {
    case empty
    case invalidLength
    case invalidAlphaNumeric
    case unsupportedCountry
    case invalidChecksum
}

Run your tests again and everything should work! One neat thing about self validating objects is Error. See our IbanParseError? By using it we make it pretty clear why our object might have failed in building. We can also make our tests even more explicit by checking for specific errors:

func test__init__invalidIbanLength__IThrow() {
  let string = "FR142"

  XCTAssertThrowsSpecific(try Iban(string), IbanParseError.invalidLength)
}

Function validation

You might wonder "Why not testing Iban.validating itself?". The answer is simple: because it is not part of our public API. We could make it public/internal but there is no reason to. That's the whole idea behind self validating object: you're asking for an object and just checking you can get it or not. The validation is... implementation details 🤷‍♂️

Conclusion

Self validating objects are nothing but simple. It is all about using objects to represent your app data and integrating the validation inside it instead of having it appart.

It might seem like a small change at first but if you try it you'll see that not only does it make your codebase safer but it also improve its readability.