Object-Oriented Programming: Beyond the Basics

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

Lesson 02: Polishing Object-Oriented Programming Concepts

Demo 2

Episode complete

Play next episode

Next
Transcript

In the last demo, you added support for marking a contact as a company contact by introducing a new property that must be set manually when the contact is created. In this demo, you’ll use inheritance to hide this flag and set it automatically from the initializers of the new types.

Open your playground. You’ll need to create two new files to represent two new classes. While many languages allow you to define multiple classes in the same file, it is often preferred to keep your classes separate to make them easier to find and work with.

Expand the navigator and select the Sources director. Click File / New / File and create a file called CompanyContactCard. Do the same for PersonContactCard.

Open CompanyContactCard.swift. Create the new class definition for CompanyContactCard:

public class CompanyContactCard: ContactCard {
}

In PersonContactCard.swift create PersonContactCard:

public class PersonContactCard: ContactCard {
}

You’ll start on the person contact first. Create a new initializer in PersonContactCard the overrides the original one in ContactCard and sets isCompany to false:

public override init(firstName: String, lastName: String, phoneNumber: String) {
  super.init(firstName: firstName, lastName: lastName, phoneNumber: phoneNumber)
  isCompany = false
}

By overriding, you are replacing the parent initializer with your own implementation. Note, you still call the parent initializer by the way of the super keyword. So when the initializer is run, the parent’s class initializer runs all the code there first, and then returns to its initializer, and runs its code.

We want to do the same with the addRelatedContact method. Override it to perform some default validation.

public override func addRelatedContact(_ contact: ContactCard) {
  if !contact.isCompany {
    print("Calling super from Person")
    super.addRelatedContact(contact)
    print("Other contact is a Person too. Adding 2-way relationship")
    contact.relatedContacts.append(contactID)
  }
}

The validation simply checks if the provided contact is a company then do nothing. A person contact can only link another person to it.

Open CompanyContactCard.swift. You’re going to do the same. Override the initializer but this time set isCompany to true:

public override init(firstName: String, lastName: String, phoneNumber: String) {
  super.init(firstName: firstName, lastName: lastName, phoneNumber: phoneNumber)
  isCompany = true
}

Then override addRelatedContact(_:):

public override func addRelatedContact(_ contact: ContactCard) {
  print("Calling super from Company")
  super.addRelatedContact(contact)
  if contact.isCompany {
    print("Other contact is a company too. Adding 2-way relationship")
    contact.relatedContacts.append(contactID)
  }
}

Both the overrides for addRelatedContact(_:) are calling super.addRelatedContact(_:). As mentioned, this actually executes the implementation that is present inside ContactCard. This class is already doing a two-way relationship if both contacts are a company or are people.

Open ContactCard.swift. Since you’ve created specific implementations of addRelatedContact, you can remove the code that checks for a company

public func addRelatedContact(_ contact: ContactCard) {
  relatedContacts.append(contact.contactID)
}

Finally, use the new types you created instead of using ContactCard. Open the main playground file. Change the definitions of all the contact objects you’re creating to use the new types:

let ehabContact = PersonContactCard(firstName: "Ehab", lastName: "Amer", phoneNumber: "1234567890")
let timContact = PersonContactCard(firstName: "Tim", lastName: "Contact", phoneNumber: "0987654321")

let kodeco = CompanyContactCard(firstName: "Kodeco", lastName: "", phoneNumber: "1111111111")
let razeware = CompanyContactCard(firstName: "Razeware", lastName: "", phoneNumber: "2222222222")

You won’t need to change anything in the calls to addRelatedContact(_:) since each type is completely responsible for creating the relationship and applying the validations. Also, you no longer need to do anything about the isCompany flag, you even don’t need to know about it.

Open ContactCard.swift. Remove the public access modifier.

public init(firstName: String, lastName: String, phoneNumber: String) {
    ...
    isCompany = false
  }

There is one more small improvement you can apply. Company contacts don’t use the last name field and the parameter firstName doesn’t look very nice in the constructor. Why not improve the look of the constructor function for CompanyContactCard.

Open CompanyContactCard.swft. Update the constructor to the following:

public convenience init(companyName: String, phoneNumber: String) {
  self.init(firstName: companyName, lastName: "", phoneNumber: phoneNumber)
  isCompany = true
}

Then change the initialization of kodeco and razeware in the playground file:

let kodeco = CompanyContactCard(companyName: "Kodeco", phoneNumber: "1111111111")
let razeware = CompanyContactCard(companyName: "Other Company", phoneNumber: "2222222222")

Run the playground. You’ll notice that the information and number of connections remain the same but the implementation looks a lot clearer.

This is good, but you can tidy it all up. Since you’re cleaning the usage of your classes a little, creating setters for properties within classes is common.

The main responsibility of setters is to perform an operation as values are being set. Setters can also provide validation or add additional functionality related to changing the value. They can also produce something known as side effects which you’ll learn about soon enough. That said, this program will build on top of the setters so we will include them now.

Open ContactCard. Create the setter for the first and last name properties:

public func set(firstName: String, lastName: String) {
  self.firstName = firstName
  self.lastName = lastName
}

We can change the name of the contact, passing in both the first and last. For instance, let’s imagine Tim prefers to be called Timothy. Return to the playground file and add the following:

timContact.set(firstName: "Timothy", lastName: "Condon")

Now create a property for the PhoneNumber. Open ContactCard and add the following:

public func set(phone: String) {
  phoneNumber = phone
}

You’re expecting the phone number to be passed as a String which is absolutely correct, but why not have the ability to pass it as an Double and internally convert it to String? Add this additional setter:

public func set(phone: Double) {
  phoneNumber = "\(phone)"
}

Take a look at the three setter functions you added.

All three are named “set” and the last two are almost identical except for the different data type of its parameter. This is called method overloading.

You can create methods with the same name, but have different number of parameters or different parameter types. And whenever you call the method, the compiler will execute the version of the method that matches the parameters you passed.

Return back to the main playground and set the phone numbers:

timContact.set(phone: "555-5555")
ehabContact.set(phone: 12345679.0)

And look at that - two phone numbers from two different types. A string for one and a double for the other.

This may look trivial but with more complex operations, this can do magic in making things easier for your team and the code readability. If you’re lazy like me, its nice to remember less method names. :]

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