Self validating models
January 05, 2021Sometimes 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.