Skip to Content
Course content

44: Protocol Composition

Click on the "Edit" button in the top corner of the screen to edit your slide content.

Imagine you're hiring for a very specific role at a company—say, a "Safety Inspector." To do this job, the person can't just be a licensed driver, and they can't just be a certified electrician. They have to be both. If they only have one of those certifications, they can't step onto the job site. They must exist at the intersection of those two sets of skills.

In Swift, we often run into the same problem. You might have a few small, focused protocols that do one thing well, but you'll eventually write a function that requires a type to satisfy multiple protocols at once. This is where Protocol Composition comes in. Instead of creating a brand new "mega-protocol" that inherits from others, you can just use the & operator to demand a combination of requirements on the fly.

Combining requirements on the fly

Let's look at a practical example. Suppose you're building a document management system. You have a Readable protocol for things that can be viewed, and a Writable protocol for things that can be edited.

protocol Readable {
    func readContent() -> String
}

protocol Writable {
    func write(content: String)
}

struct Note: Readable, Writable {
    var text: String = "Hello!"
    func readContent() -> String { return text }
    func write(content: String) { print("Writing \(content)...") }
}

struct ReadOnlyFile: Readable {
    func readContent() -> String { return "I am read-only." }
}

Now, imagine you have a function called syncDocument. This function needs to read the content from one place and write it to another. It can't work with just a Readable object (because it needs to write) and it can't work with just a Writable object (because it needs to read). It needs something that is both.

Here is how you compose those protocols:

func syncDocument(document: Readable & Writable) {
    let content = document.readContent()
    document.write(content: "Synced: \(content)")
}

let myNote = Note()
let myFile = ReadOnlyFile()

syncDocument(document: myNote) // Works perfectly!
// syncDocument(document: myFile) // Compiler error: ReadOnlyFile does not conform to Writable

Why not just make a new protocol?

I've had students ask me, "Why not just create a protocol ReadWrite: Readable, Writable {}?" You absolutely can, and if you find yourself using Readable & Writable in twenty different places, you probably should.

But composition is powerful because it prevents "protocol bloat." If you have five different small protocols, creating every possible combination of them as new named protocols would lead to a combinatorial explosion of names. ReadableAndWritable, ReadableAndSearchable, WritableAndSearchable, ReadableAndWritableAndSearchable... it gets messy fast. Using the & operator keeps your codebase lean by defining the requirement exactly where it's needed.

Using typealiases for readability

If a composition is used frequently but doesn't feel like a full-blown "entity" in your domain, I usually recommend a typealias. It gives you the best of both worlds: a clean name for the requirement without the overhead of a formal protocol hierarchy.

typealias ReadWriteDocument = Readable & Writable

func archiveDocument(document: ReadWriteDocument) {
    // Logic here
}

It's a subtle distinction, but it makes your intent clear. You're telling other developers, "I don't care what this object is; I just care that it can be read and written to."




📋 Practical Task

Building a Secure Storage Coordinator

You are building a security module for an app. You have two protocols: Encryptable (which requires a method encrypt(_ data: String) -> String) and Storable (which requires a method save(data: String)).

Your Task:

  • Define the Encryptable and Storable protocols.
  • Create a struct called SecureVault that conforms to both.
  • Create a struct called PublicFolder that conforms only to Storable.
  • Write a function called persistSecretly that takes a parameter. Use protocol composition to ensure the parameter must be both Encryptable and Storable.
  • Inside persistSecretly, call the encrypt method and then pass that result into the save method.
  • Call the function using an instance of SecureVault to verify it works, and attempt to call it with PublicFolder to confirm the compiler stops you.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.