Fun with protocols

Back in the old days, people used to communicate with each other by shouting.

struct Person {
    let name: String    
    
    func sendMesage(content: String) {
        print(content)
    }
}

But soon, people found that shouting was not practical. The receiver of a message could just be too far away, or sleeping, so humanity invented written messages. And so snail mail was born.

struct Person {
    let name: String
    
    let mail = SnailMail()
    
    func sendMesage(content: String) {
        mail.send(message: content)
    }
}

struct SnailMail {
    func send(message: String) {
        print("sending snail mail. ", message)
    }
}

Fast forward a few hundred years, to the early ages of the internet. Someone, somewhere, invented email, so now people could communicate like this.

struct Person {
    let name: String
    
    let emailClient = Email()
    
    func sendMesage(content: String) {
        emailClient.send(message: content)
    }
}

struct Email {
    func send(message: String) {
        print("sending email ", message)
    }
}

Soon, people realized that even though email was great, there were other forms of communication. What’s even more remarkable, people were happy sending emails, but they soon realized that what they really wanted was to be able to do something slightly different. Instead of sending emails, they just wanted to send messages.

Some of those messages would be emails, some of them would be snail mail, some others might even be some unknown technological marvel soon to be discovered. But what all people knew is that they did not wan to be constrained by being able to send emails and only emails.

And here is when people realized that the way they had been approaching communications was slightly unadaptable.

One of the few software engineers on the face on earth at that time, decided to do something about it. She started thinking about what people wanted, and why they could not have it, and soon realized that the actual problem was that there was a hardcoded dependency between a Person, and the way that person was able to communicate.

Our hero also realized that the reason why that was a problem was that what people really wanted was freedom of choice, and flexibility, and being able to send messages by multiple different mechanisms, not only email.

And that made her think: “maybe what people expect are not things, but behaviours”. Indeed, People seemed to have a clear expectation: “we should be able to send a message”, but different people wanted that expectation to be fulfilled differently.

Our hero thought “what people want to rely upon is an abstraction, not something specific.”1

So our hero started building her solution around an abstraction: a protocol defining a behaviour.

protocol Sendable {
    func send(message: String)
}

That allowed our hero to model a couple of specific implementations of that behaviour 2. For example, an email, an SMS, an iMessage (aka Apple Message, to avoid lowercasing a class, apparently all our hero firmly believes in coding standards)

struct Email: Sendable {
    func send(message: String) {
        print("sending email ", message)
    }
}

struct SMS: Sendable {
    func send(message: String) {
        print("sending SMS ", message)
    }
}

struct AppleMessages: Sendable {
    func send(message: String) {
        print("sending iMessage ", message)
    }
}

So now, a Person could be taught to blindly rely on the abstraction our hero modelled, trusting it to know how to fulfill her expectations.3

struct Person {
    let name: String
    /**
    * Person does not know exactly what it depends on. All it knows is that it depends on something that willl provide a send() method (as declared in the protocol)
    */
    let sendable: Sendable
    
    /**
    * Inject the dependency
    */
    init(name: String, sendable: Sendable) {
        self.name = name
        self.sendable = sendable
    }
    
    /**
    * And rely on the abstraction, provided as a parameter (aka injected), to do its thing
    */
    func sendMesage(content: String) {
        sendable.send(message: content)
    }
}

That way, Cesar can send emails while Jodelle can send emails, or a hipster can send a Messenger Pigeon.

/**
 * Cesar Sends SMSs
 */
let smsSender = SMS()
let me = Person(name: "Cesar", sendable: smsSender)
me.sendMesage(content: "Hey Jodelle!")

/**
 * Jodelle Sends Emails
 */
let emailSender = Email()
let jodelle = Person(name: "Jodelle", sendable: emailSender)
jodelle.sendMesage(content: "Hey Cesar!")

/** 
 * Hipsters send pigeons
 */
struct Pigeon: Sendable {
    func send(message: String) {
        print("sending this message with a pigeon ", message)
    }
}

let pigeon = Pigeon()
let aHipster = Person(name: "It's ironic!", sendable: pigeon)
aHipster.sendMesage(content: "Hello world")

Our hero was satisfied with her implementation of the solution, specially because she realized that this design allowed her to provide as many means of sending messages as she wanted, without having to change the way a Person sends a message.4

Also, our hero soon realized that she could only test that messages are actually sent, by providing a very specific type of Sendable that did not actually send any message.5

/**
 *  Mock objects usually do not have any actual behaviour, other than fulfilling an expectation. In this case, we want this mock object to mark itself as "sent" when the send() method is called
 */
class MockSendable: Sendable {
    var sent: Bool = false
    func send(message: String) {
        sent = true
    }
}

/**
 * Now we inject the mock into a Person, ask that person to send a mesage, and check that the mock has been marked to "sent". That, in the context of unit testing, is called the AAA (Arrange, Act, Assert)
 */
 
//Arrange. Create a Mock, and inject it
let mock = MockSendable()
print("mock initial state ", mock.sent)

let personUnderTest = Person(name: "Test", sendable: mock)

// Act. Call the method that triggers the behaviour we want to test
personUnderTest.sendMesage(content: "Testing testing")

// Assert. Verify that our expectation (the mock is marked as sent) has been fulfilled. This way, we can test Person in isolation, without any dependencies
print("Did the mock get called? ", mock.sent)

And that’s the power of protocols. A protocol defines a contract, declares a behaviour, but does not define the way that contract of behaviour is implemented. A protocol is abstraction, and the details, the nitty gritty of how those contracts or behaviours are implemented are left to concrete implementations of the protocol.

Relying on abstractions instead of concretions, in general, makes designs more decoupled, open for extension and closed for modification, and highly testable.

  1. Also known as the Dependency Inversion Principle
  2. What we call polymorphism
  3. This is what we call Dependency Injection
  4. AKA the Open/Closed Principle
  5. These are test doubles

One Reply to “Fun with protocols”

Leave a Reply

Your email address will not be published. Required fields are marked *