Object-Oriented Programming: Beyond the Basics

Oct 17 2023 · Swift 5.9, iOS 17, Xcode 15

Lesson 05: Liskov Substitution, Interface Segregation & Dependency Inversion

Demo 1

Episode complete

Play next episode

Next
Transcript

In this demo, you’ll put the Liskov Substitution to work. You’ll pick up right where you left off with the contacts app in the previous lesson.

Your new requirement is to implement EmergencyContactCard. It can’t have any related contacts or be related to any, so you’ll use the configuration implemented in the last demo to disable those features. The main requirement on this new card is that the phone number must be exactly three digits.

Create a new file under Sources named EmergencyContactCard.swift and add the following:

public class EmergencyContactCard: ContactCard {
  override private init(firstName: String, lastName: String, phoneNumber: String) {
    super.init(firstName: firstName, lastName: lastName, phoneNumber: phoneNumber)
    canAdd = []
    cardType = .emergency
  }

The first initializer is the one you’re familiar with from the base class but, in this case, it’s marked as private.

 public convenience init?(emergencyName: String, phoneNumber: String) {
  if phoneNumber.count != 3 {
    return nil
  }
    self.init(firstName: emergencyName, lastName: "", phoneNumber: phoneNumber)
  }

Here, you used a ? in the name of the convenience initializer to mark it as a fail-able initializer. You also added a validation that checks if the string length equals three characters. If it doesn’t, then the initialization fails and returns nil. If it succeeds, the initialization process continues as normal.

 public override func contactInformation() -> String {
    "Contact: EmergencyName: \(firstName), Phone: \(phoneNumber)"
  }
}

Finally, the override for contactInformation() just returns a string properly that represents the new type. It ignores the number of connections because it won’t have any. So far, you have kept the Liskov Substitution principle!

The next step is to include the validation in set(phone:):

public override func set(phone: String) {
  guard phone.count == 3 else {
    return
  }
  super.set(phone: phone)
}

This is where you start breaking the principle!

The system expects the value to be stored when set(:) is called. However, your child type, EmergencyContactCard, ignores the value completely when the input doesn’t match its expectations.

This is clearly the requirement, but the system isn’t expecting operations to be ignored.

One way to fix this problem is to allow the base type to inform the system whether or not update operations succeed by providing a Bool representing success or failure.

Open ContactCard.swift. Change the signature of all set(:) functions to return a Bool:

public func set(firstName: String, lastName: String) -> Bool {
  self.firstName = firstName
  self.lastName = lastName
  broadcastUpdates()
  return true
}

public func set(phone: String) -> Bool {
  phoneNumber = phone
  broadcastUpdates()
  return true
}

public func set(phone: Double) -> Bool {
  phoneNumber = "\(phone)"
  broadcastUpdates()
  return true
}

Now, return to EmergencyContactCard. Update the override of set(phone:) to return false if the length didn’t meet the expectations:

public override func set(phone: String) -> Bool {
  guard phone.count == 3 else {
  return false
  }
  return super.set(phone: phone)
}

Now, test this by creating an instance. Open the main playground file. Add a new emergency contact.

let emergencyContact1 = EmergencyContactCard(emergencyName: "Cold Store Creamery", phoneNumber: "555")

Just a quick note. If you are a getting an error that states that the EmergencyContactCard could not be found in scope, you may need to restart your playground and/or rebuild the project. Next, create another one with an empty phone number.

let emergencyContact2 = EmergencyContactCard(emergencyName: "Rifftrax", phoneNumber: "")

Finally, print out the contact information.

print("emergency 1: \(emergencyContact1?.contactInformation() ?? "None")")
print("emergency 21: \(emergencyContact2?.contactInformation() ?? "None")")

Run the playground. The first contact prints, but the second one doesn’t. The child class — EmergencyContactCard — delivers the requirements without performing any functionality that isn’t specified by the base class. New requirements MUST be implemented.

It’s also important to know how to update the overall system to accommodate future additions. The first time you designed the base class, there were no requirements for data validations, so you didn’t need to consider them. When validations became a requirement for a new child type, you made sure that the system also received the necessary updates to make sure everything fit together properly.

If you implement that validation without modifying the system, you risk breaking consistency among the child types. Later on, a team member might add a feature to your system that fails to display the correct information to a user attempting to update their phone number. This could lead to confusion, as the user might assume the save operation was successful when it wasn’t. The end result would be a bug causing the loss of the new emergency number.

See forum comments
Cinema mode Download course materials from Github
Previous: Instruction 1 Next: Instruction 2