Stoian Dan
Posted on January 3, 2023
Introduction
Imagine we want to implement a sort of postal system, where messages can be sent from someone to the post office; where it will be accordingly dispatched to the right destination.
Beginning
We could go ahead and define a simple data model, that represents a message.
This entity holds the actual message (we'll call it content), and some meta-data, i.e. data about the message itself.
Since we want this message to be immutable and we don't want shared references of it, we can make it a struct, as opposed to a class:
struct Message {
// the meta-data; who is sending this message
let sender: String
// also meta-data; the receiver of the message
let to: String
// the actual content of the message
let content: String
}
The Dispatcher
Now that we have a data model, which will be a shared entity between sender (a person) and dispatcher (the postal office).
We can go ahead and imagine our dispatcher:
class Dispatcher {
public func dispatch(_ message: Message) {
print("Sending \(message.content) to \(message.to) from \(message.from)")
}
}
The Sender
Finally let's imagine the person sending the message:
class Person {
private unowned let dispatcher: Dispatcher
let name: String
init(dispatcher: Dispatcher, ) {
self.dispatcher = dispatcher
self.name = name
}
public func sendMessage(of message: Message) {
dispacher.dispatch(message)
}
}
Example
This looks ok. Let's see how it will look in practice:
let dispatcher = Dispatcher()
let helvidius = Person(dispatcher: dispatcher, name: "Helvidius")
let jovinian = Person(dispatcher: dispatcher, name: "Jovinian")
helvidius.sendMessage(of: Message(sender: helvidius.name, to: "Jovinian", content: "Hi Jovinian! How are you?"))
The actual problem
Notice however, it's a bit odd that Bob has to specify he's the sender.
A person has a name, and if a person sends a message, he's always going to be the sender.
In fact, the current code could be exploited, supposing we'd offer anyone the ability to send messages (i.e. Person class would be a public API):
let dispatcher = Dispatcher()
let evilMike = Person(dispatcher: dispatcher, name: "Evil Mike")
evilMike.sendMessage(of: Message(sender: "Andy", to: "Sandy", content: "Hi Sandy! I'm Andy, and I think you look ugly!"))
The solution!
Protocols could make our lives much more easy. To start, we've noticed every person has a name; it also s seems maybe not just persons can send messages. why not have machines be able to send messages as well? All we need after all, besides the content and receiver, is for the sender to identify himself.
We could use Swift's UUID type, and that would be a great idea! However, for the sake of simplicity, well stick to a name field, of type string:
protocol Sender {
var name: String { get }
}
Great! Now we could have a ton of other types that can be senders, like machines:
class Machine: Sender {
let name: String
let dispacher: Dispatcher
init(name: String, dispathcer: Dispathcer) {
self.name = name
self.dispathcer = dispatcher
}
public sendHappyNewYear() {
let date = Date.now
let components = Calendar.current.dateComponents([.month, .day], from: date)
if components.day == 1 && components.month == 1 {
dispatcher.dispatch(Message(sender: "Robot1", to: "Sandy", content: "Happy new Year Sandy!"))
}
}
}
However, the actual problem still remains, just about anyone can create a message and pretend to be someone else!
So here's where POP (Protocol Oriented Programming) comes more handy in place.
We can make the initializer (or constructor, if you will) of Message entity fileprivate!
struct Message {
// the meta-data; who is sending this message
let sender: String
// also meta-data; the receiver of the message
let to: String
// the actual content of the message
let content: String
fileprivate init(sender: String, to: String, content: String) {
self.sender = sender
self.to = to
self.content = content
}
}
Great, now we can define an extension method on the Sender protocol. That is a method that anyone who's a sender, can automatically benefit from, even retroactively! I.e. entities that already conform to the Sender protocol before the extension method is introduced can benefit from!:
struct Message {
...
}
extension Sender {
func createMessage(to receiver: String, content message: Message) -> Message {
Message(sender: self.name, to: receiver, content: message)
}
}
Now any sender can call the createMessage
method, that will automatically use the name the Sender
has, because remember, the only thing we know about a Sender
is that he has a name, and we can make use of that name in protocol extensions.
Posted on January 3, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.