Extending your protocols with default behavior

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, every object that is HerbivoreType has a favoriteMeat variable. This means that you have to duplicate this variable in every object that conforms to HerbivoreType. Usually, you want as little code repetition as possible, and repeating a variable over and over again 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 every class, subclass, and superclass all of the time. This means that changing or removing a specific 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 you can take is to immediately define the method on a protocol extension without adding it to the protocol's requirements:

extension Domesticatable {
  func printHomeAddress() {
    if let address = homeAddress {
      print(address)
    }
  }
} 

By defining the printHomeAddress() method in the 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 shows an example of such odd results 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 you call myPigeon.printHomeAddress(), the custom implementation is used to print the address. However, if you define a function, such as printAddress(animal:), that takes a Domesticatable object as its parameter, the default implementation provided by the protocol 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 same snippet as in the preceding section, but change the Domesticatable 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 you just saw.

Protocol extensions can't hold stored properties. This means that you can't add your variables to the protocol 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 to avoid duplicating the same variable in multiple places. Let's take a look at an example:

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 you want to be able to check whether a Domesticatable has a home address, you 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 you don't have to add this property to all 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 default behaviors in protocol extensions makes the protocol-oriented approach we've seen before even more powerful; you can essentially mimic a feature called multiple inheritance without all the downsides of subclassing. Simply adding conformance to a protocol can add all kinds of functionality to your objects, and if the protocol extensions allow it, you won't need to add anything else to your code. Let's see how you can make protocols and extensions even more powerful with associated types.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset