Protocol composition is the process of combining multiple protocols into a single protocol. You can think of it as multiple inheritance.
With protocol DoSomething
, below, class HasSomethingToDo
is able to do whatShouldBeDone
and anotherThingToBeDone
.
// First approachprotocol DoSomething {func whatShouldBeDone()func anotherThingToBeDone() -> String}// Class conforms to 'DoSomething'class HasSomethingToDo: DoSomething {func whatShouldBeDone() {print("I did Something!")}func anotherThingToBeDone() -> String {print("another thing that I did")return " "}}
What if we have a class
that just wants to do whatShouldBeDone
or another which needs to do just anotherThingToBeDone
? The protocol DoSomething
will contain unimplemented requirements for these other classes. An even bigger problem with the approach above is that it violates the
// First protocol definedprotocol DoSomething {func whatShouldBeDone()}// Second protocol definedprotocol DoAnotherThing {func anotherThingToBeDone()}// A class conforms to first protocolclass JustDoSomething: DoSomething {func whatShouldBeDone() {print("I did Something!")}}// Another class conforms to the second protocolclass JustDoAnotherThing: DoAnotherThing {func anotherThingToBeDone() {print("I do something else thing!")}}let classInstance = JustDoSomething()classInstance.whatShouldBeDone() // I did Something!//classInstance.anotherThingToBeDone() // will not existlet secondClassInstance = JustDoAnotherThing()secondClassInstance.anotherThingToBeDone() // I do something else thing!//secondClassInstance.anotherThingToBeDone() // will not exist
Now, what happens to our initial class HasSomethingToDo
, which actually requires the two abilities?
This is where we introduce the protocol composition type concept. In the basic approach, a generic object-oriented programming methodology, we create an empty protocol that inherits from the other two protocols, thus making our class conform to this new protocol.
// first protocol definedprotocol DoSomething {func whatShouldBeDone()}//second protocol definedprotocol DoAnotherThing {func anotherThingToBeDone()}// The new protocol which inherits from the two protocols from above.protocol DoTheseTwoThings: DoSomething & DoAnotherThing {}// The class conforms to the new protocol.class HasSomethingToDo: DoTheseTwoThings {func whatShouldBeDone() {print("I did Something!")}func anotherThingToBeDone() {print("Another thing to be done!")}}let newInstance = HasSomethingToDo()newInstance.whatShouldBeDone()newInstance.anotherThingToBeDone()
Swift provides a more “Swifty” way to achieve protocol composition. We are able to combine two or more protocols by using the &
operator.
If you observe the earlier examples, we defined two protocols, DoSomething
and DoAnotherThing
, and those two protocols are now adopted by class HasSomethingToDo
. We combine these multiple protocols into a single instance and call it specificActions
using protocol composition.
// first protocol definedprotocol DoSomething {func whatShouldBeDone()}//second protocol definedprotocol DoAnotherThing {func anotherThingToBeDone()}class HasSomethingToDo: DoSomething, DoAnotherThing {func whatShouldBeDone() {print("I did Something!")}func anotherThingToBeDone() {print("Another thing that I did!")}}let specificActions: DoSomething & DoAnotherThing = HasSomethingToDo()// we can now accessspecificActions.whatShouldBeDone()specificActions.anotherThingToBeDone()
I suggest using the “Swifty” approach, because we can avoid creating a new empty protocol, only to combine different protocols and use a typealias
to clean the concatenation of protocols when it gets a little messy.
There is also opaque type, which differs from protocol type in that it refers to a particular concrete type, whereas protocols can refer to as many types as can conform to it. So opaque type preserves type identity, while protocol type does not, and it’s more flexible about its return type.