A Functional Approach to Validation using Protocol Oriented Programming

I wrote an article about how I took a functional approach to implement some UITextField validation. In this article I detailed how the functional approach helped me defining small functions easy to use and test. After thinking about the implementation I realized that the code worked great but was only available for UITextField. It wasn’t reusable for anything else that could need the same kind of validation. I basically broke an important protocol-oriented maxim: if you want to create a class, create a protocol first.

This time, instead of creating a class extension, I will start with protocols:

protocol Validatable {
    associatedtype T

    func validate(_ functions: [T]) -> Bool
}

protocol Evaluatable {
    associatedtype T

    func evaluate(with condition: T) -> Bool
}

The evaluate and validate features are now available for anyone conforming to them. Note that I’m using associated types to make the validation and evaluation types generic to increase their genericity.

Let’s now revisit the UITextField example using these protocols:

extension UITextField: Validatable {
    func validate(_ functions: [(String) -> Bool]) -> Bool {
        return functions.map { f in f(self.text ?? "") }.reduce(true) { $0 && $1 }
    }
}

We extend the UITextField class to conform to Validatable and the validation logic stays the same. The associated type is defined here as a String -> Bool function.

extension String: Evaluatable {
    func evaluate(with condition: String) -> Bool {
        guard let range = range(of: condition, options: .regularExpression, range: nil, locale: nil) else {
            return false
        }

        return range.lowerBound == startIndex && range.upperBound == endIndex
    }
}

We extend the String class to conform to Evaluatable and implement the same logic as before. The associated type is defined here as a String type.

Now we can use the evaluation functions exactly the same way as before:

func isCVCValid(text: String) -> Bool {
    let regexp = "^[0-9]{3,4}$"
    return text.evaluate(with: regexp)
}

let cvcTextField = UITextField()
cvcTextField.text = "123"
cvcTextField.validate([isCVCValid])

Another Use for Our Protocols

Let’s see how using protocols can be beneficial for a complete different part of your code. Let’s take an example with a User model that you want to validate:

struct User {
    let firstName: String
    let lastName: String
    let age: Int
}

We can make User conform to our Validatable protocol:

extension User: Validatable {
    func validate(_ functions: [(User) -> Bool]) -> Bool {
        return functions.map { f in f(self) }.reduce(true) { $0 && $1 }
    }
}

The associated type is defined as a User -> Bool function because we want to take a user as a parameter and return a boolean value to validate it.

Let’s also define the evaluation functions we will use:

func isUserNameValid(user: User) -> Bool {
    let regexp = "[A-Za-z] "
    return user.firstName.evaluate(with: regexp) && user.lastName.evaluate(with: regexp)
}

func isUserAdult(user: User) -> Bool {
    return user.age >= 18
}

Finally we can create a user and test the validation feature:

let user = User(firstName: "Thibault", lastName: "Klein", age: 25)
XCTAssertTrue(user.validate([isUserNameValid, isUserAdult]))

The validation and evaluation features are now available for another type that I defined. On top of that it still keeps the benefits of functional programming I described in my previous article. This example might not be the most useful one possible, but it gives you a sense that anything can be validated now that I moved all the logic into protocols.

I definitely recommend thinking towards protocol first the next time you want to introduce a new feature in your code, as it can make it more flexible, descriptive, and reusable in the future.

Categories: