The previous examples have mainly used variables as the requirements for protocols. One slight downside of protocols is that they can result in a bit of code duplication. For example, any object that is a HerbivoreType has a favoriteMeat variable. This means that you have to duplicate this variable in every object that conforms to the HerbivoreType. Usually, you would want as little code repetition as possible, and duplicating a variable like this might seem like a step backward.
Even though it's nice if you don't have to declare the same property over and over again, there's a certain danger in not doing this. If your app grows to a large size, you won't remember each and every class, subclass, and superclass all of the time. This means that changing or removing a certain property can have undesired side effects in other classes.
Declaring the same properties on every object that conforms to a certain protocol isn't that big a deal; it usually takes just a few lines of code to do this. However, protocols can also require certain methods to be present on objects that conform to them. Declaring those over and over again can be cumbersome, especially if the implementation is the same for most objects. Luckily, you can make use of protocol extensions to implement a certain degree of default functionality.
To explore protocol extensions, let's move the printHomeAddress function into the Domesticatable protocol so all Domesticatable objects can print their own home addresses. The first approach we can take is to immediately define the method on a protocol extension:
extension Domesticatable {
func printHomeAddress() {
if let address = homeAddress {
print(address)
}
}
}
By defining the printHomeAddress method in this protocol extension, every object that conforms to Domesticatable has the following method available without having to implement it with the object itself:
let myPidgeon = Pigeon(favoriteMeat: "Insects",
favoritePlant: "Seeds",
homeAddress: "Leidse plein 12, Amsterdam")
myPidgeon.printHomeAddress() // "Leidse plein 12, Amsterdam"
This technique is very convenient if you want to implement default behavior that's associated with a protocol. You didn't even have to add the printHomeAddress method as a requirement to the protocol. However, this approach will give you some strange results if you're not careful. The following snippet illustrates the problem by adding a custom implementation of printHomeAddress to the Pigeon struct:
struct Pigeon: Bird, FlyingType, OmnivoreType, Domesticatable {
let favoriteMeat: String
let favoritePlant: String
let homeAddress: String?
func printHomeAddress() {
if let address = homeAddress {
print("address: \(address.uppercased())")
}
}
}
let myPigeon = Pigeon(favoriteMeat: "Insects",
favoritePlant: "Seeds",
homeAddress: "Leidse plein 12, Amsterdam")
myPigeon.printHomeAddress() // address: Leidse plein 12, Amsterdam
func printAddress(animal: Domesticatable) {
animal.printHomeAddress()
}
printAddress(animal: myPigeon) // Leidse plein 12, Amsterdam
When we call myPigeon.printHomeAddress, the custom implementation is executed. However, if we define a function, printAddress, that takes a Domesticatable object as its parameter, the default implementation is used.
This happens because printHomeAddress isn't a requirement of the protocol. Therefore, if you call printHomeAddress on a Domesticatable object, the implementation from the protocol extension is used. If you use the exact same snippet as in the preceding section, but adapt the protocol as shown in the following code, both calls to printHomeAddress print the same thing, that is, the custom implementation in the Pigeon struct:
protocol Domesticatable {
var homeAddress: String? { get }
func printHomeAddress()
}
This behavior is likely to be unexpected in most cases, so it's usually a good idea to define all methods you use in the protocol requirements unless you're absolutely sure you want the behavior we've just explored.
Protocol extensions can't hold stored properties. This means that you can't add your variables to the protocol in order to provide a default implementation for them. Even though extensions can't hold stored properties, there are situations where you can still add a computed property to a protocol extension in order to avoid duplicating the same variable in multiple places. Let's take a look at an example (the updated code is highlighted):
protocol Domesticatable {
var homeAddress: String? { get }
var hasHomeAddress: Bool { get }
func printHomeAddress()
}
extension Domesticatable {
var hasHomeAddress: Bool {
return homeAddress != nil
}
func printHomeAddress() {
if let address = homeAddress {
print(address)
}
}
}
If we want to be able to check whether a Domesticatable has a home address, we can add a requirement for a Bool value, hasHomeAddress. If the homeAddress property is set, hasHomeAddress should be true; otherwise it should be false. This property is computed in the protocol extension, so we don't have to add this property to all of our Domesticatable objects. In this case, it makes a lot of sense to use a computed property because the way its value is computed should most likely be the same across all Domesticatable objects.
Implementing a default behavior in protocol extensions makes the protocol-oriented approach we've seen before even more powerful; we can essentially mimic multiple inheritances this way without all the downsides of subclassing. Simply adding conformance to a protocol can add all kinds of functionality to your objects, and if the extensions allow it, you won't need to add anything else. Let's see how we can make the most of our protocols and extensions by making them generic with associated types.