Object-oriented Programming with Swift – Part 2

Initialization and Deinitialization

Initialization is when we prepare to create an instance of a class. This process may involve configuring the properties or any other miscellaneous setup required before we can use that instance like opening a database connection, creating a save file, or opening a web socket.

BUILD GAMES

FINAL DAYS: Unlock 250+ coding courses, guided learning paths, help from expert mentors, and more.

In Swift, we use initializers to run some code before the instance is created. These initializers look like methods but have a very specific method signature: they must be named init and return nothing. We use them to simply setup the instance, and Swift will handle the act of creating the resources for that instance for us.

Let’s see how we can create an initializer in Swift.

class Game {
    var numberOfPlayers = 2
    init() {
        print("Starting game...")
    }
}

var game1 = Game()

Since our initializer does not take any parameters, we use empty parentheses. Now when we create an instance of game, we should see “Starting game…” appear. It is important to note that an initializer is called each time we create an instance of a class.

Notice that we have a property the defines the number of players our game has. If we wanted our game to have 4 players, we would currently have to do the following in two lines of code.

var game1 = Game()
game1.numberOfPlayers = 4

However, remember that initializers can be used to configure properties! We can create another initializer that overloads this one and takes a parameter. Method overloading is when we have two methods with the same name, but different parameters or return types. Let’s create another initializer to configure the number of players.

class Game {
    var numberOfPlayers = 2
    init() {
        print("Starting game...")
    }
    
    init(numberOfPlayers: Int) {
        self.numberOfPlayers = numberOfPlayers
    }
}

We have a second initializer with the same name, but different parameters. This is ok because Swift can use the parameters and return type to differentiate which method we’re referring to. Now instead of two lines of code, we can use a single line of code to create an instance of game with 4 players.

var multiplayer = Game(numberOfPlayers: 4)

Notice that the parameter-less initializer is not executed when we overload this initializer. We can categorize initializers into designated and convenience initializers. The two initializers that we declared are designated initializersConvenience initializers are required to call a designated initializer. If we wanted to make the parameter initializer call the parameter-less initializer, we can convert it into a convenience initializer by adding the convenience modifier.

class Game {
    var numberOfPlayers = 2
    init() {
        print("Starting game...")
    }
    
    convenience init(numberOfPlayers: Int) {
        self.init()
        self.numberOfPlayers = numberOfPlayers
    }
}

Now when we create a new instance of Game with the convenience initializer, we call the designated initializer.

We can also have failable initializers that use optionals. Instead of returning a definite instance, we can have the initializer fail and return nil. We can make the convenience initializer into a failable initializer and have it return nil if the number of players provided is less than zero.

class Game {
    var numberOfPlayers = 2
    init() {
        print("Starting game...")
    }
    
    convenience init?(numberOfPlayers: Int) {
        if numberOfPlayers < 0 {
            return nil
        }
        self.init()
        self.numberOfPlayers = numberOfPlayers
    }
}

We can convert a regular initializer into a convenience initializer by changing the name to be init?. Now when we create an instance, we get an optional!

The complimentary process to initialization is deinitialization. This happens when Swift re-collects the memory and resources of an instance. This process usually cleans up the instance like closing any connections and writing to files.

We can create a deinitializer by using the deinit keyword. They’re similar to initializers because they require a specific name, parameters, and return type. Let’s create a deinitializer for our game class.

class Game {
    ...
    deinit {
        print("Quitting game...")
    }
}

To see the deinitializer in action, we have to create an optional instance of Game. We can use the failable initializer to do that!

var multiplayer = Game(numberOfPlayers: 4)
multiplayer = nil

When we set an optional to nil, we invoke the deinitializer! In most cases, we won’t need an explicit deinitializer, but it is ideal for closing connections, saving, or undoing code in the initializer!

Inheritance

One of the fundamental object-oriented principles is inheritance. Inheritance is a concept we use for creating hierarchies of classes. Consider a real-world example. All dogs are mammals and inherit traits of mammals. Similarly, all humans are also mammals and inherit traits of mammals as well. However, dogs and humans have their own traits that make them unique. We can model this same hierarchy in software using inheritance.

We can have a base class or superclass called Mammal and create a subclass Dog that inherits all of mammal’s methods and properties.

class Mammal {
    ...
}

class Dog: Mammal {
    ...
}

Dog is a subclass of Mammal and Mammal is the superclass of Dog. The properties and methods of Mammal are accessible from the Dog subclass. For example, let’s add some methods and properties.

class Mammal {
    var numberOfLegs = 4
    var hasTail = true
    
    func speak() {
        print("Speaking!")
    }
}

class Dog: Mammal {
}

let fido = Dog()
fido.speak()
fido.numberOfLegs
fido.hasTail

Notice that Dog is just an empty class, but we can access and use the properties and methods of Mammal since Dog inherits from Mammal.

Something isn’t quite right though: the speak method. We want each subclass to define its own behavior for speaking. We can have our Dog subclass provide its own behavior for the speak method by overriding it. When we override a method, Swift will use the code in our overridden method instead of the superclass’s code. We can override a method in Swift using the override modifier.

class Dog: Mammal {
    override func speak() {
        print("Woof!")
    }
}

Now if we ran the speak method on an instance of Dog, we would see “Woof!” printed! In addition to overriding methods, we can also override property getters and setters to provide our own implementation.

If we wanted to run code in the superclass, we have access to the super keyword, which refers to the superclass. We could execute the superclass’s speak method by typing super.speak(). If our superclass did some extra initialization for us that we wanted to take advantage of, we could use the super keyword and execute that code.

We can prevent subclasses from overriding our methods or properties by using the final modifier.

class Mammal {
    var numberOfLegs = 4
    var hasTail = true
    
    final func speak() {
        print("Speaking!")
    }
}

The Dog class should throw an error because we cannot override properties or methods that are declared with the final modifier.

Access Modifiers

Access modifiers are modifiers we put on classes, methods, and properties to limit their visibility. This allows us to hide one part of code from other parts of our code. Swift code can be split across multiple files and can have multiple classes in the same file. This gives us, the programmer, flexibility in how we write our code. We’ll talk about a few modifiers like public, internal, fileprivate, and private.

The public modifier simply means that anyone can access the property, class, or variable or call the method or function. This is the most open of all of the access modifiers.

The internal modifier restricts access to within our module. (A module is a group of Swift files containing related classes.) This is the default access modifier! Unless we have a specific reason to use this modifier, we should stick with the others described in this section.

The fileprivate modifier restricts access to within the file that the thing is defined in. We have this access modifier so that we can declare different classes that can share a variable using the fileprivate modifier.

Finally, the most restrictive modifier is the private modifier which restricts access to within the class only. Any property or method declared with the private modifier is inaccessible, either for reading or writing, outside of the class. For example, let’s make the numberOfLegs property private.

class Mammal {
    private var numberOfLegs = 4
    var hasTail = true
    
    func speak() {
        print("Speaking!")
    }
}

let fido = Dog()
fido.speak()
fido.numberOfLegs // error!
fido.hasTail

Even though Dog is a subclass, since numberOfLegs was defined in the Mammal class, we can’t access it outside of that class!

This discussion on access modifiers leads us to our second object-oriented programming principle: encapsulation or data hiding. Encapsulation is the practice of keeping the internal state of an object private while providing methods to all the outside world to access that internal state in a safe way. Take a look at the following diagram.

3-5-encapsulationHere we’re modeling a car. We have the internal state of the car kept inside of the inner circle. The outer shell is that of the different methods that we have, called getters and setters. In our case, we use Swift’s built-in property getters and setters for this. Using encapsulation, we only allow access to the internal state of our car in a safe way through methods or getters and setters. This helps maintain constraints on our state and prevents someone from changing our state to an invalid state.

For example, if we exposed the state to the world, we might accidentally change the gear to a negative value! Instead, we use a setter and only change the internal state if the input is valid. This concept also works with getters. For example, suppose it is more efficient internally to keep color stored as a low-level representation using raw bytes. That’s not human-readable at all! However, in the getter, we could do the conversion to make the color human-readable and useable!

As a general rule, we should always follow encapsulation and keep the internal state of our class internal using the private access modifier, and use methods, or getters and setters, to allow accessing and mutating that state.

Fundamental object-oriented programming principles

We’ve already seen two principles of object-oriented programming: inheritance and encapsulation. We’re going to discuss abstraction and polymorphism. Let’s go back to our mammal class and create several different subclasses with their own speak method.

class Mammal {
    func speak() {
        print("Speaking!")
    }
}

class Dog: Mammal {
    override func speak() {
        print("Woof!")
    }
}

class Cat: Mammal {
    override func speak() {
        print("Meow!")
    }
}

class Human: Mammal {
    override func speak() {
        print("Hello!")
    }
}

We know, since Mammal is a superclass, we can declare a variable of type Mammal and assign it to an instance of any of its subclasses.

let fido: Mammal = Dog()
let mittens: Mammal = Cat()
let bob: Mammal = Human()

All of the above variables are of type Mammal. Let’s see what happens when we execute the speak method.

fido.speak()
mittens.speak()
bob.speak()

We might expect to just see “Speaking” printed, but we get the correct string printed to the screen! This is because of polymorphism! It allows us to abstract the implementation details of the speak method! Even though our variables are of type Mammal, Swift knows that the type of the instance has its own speak method and will use that in place of Mammal’s implementation. This allows us to abstract the implementation details of each individual subclass’s speak method into the superclass’s method. We can use the speak method and know that Swift will call the right one!

In this section, we discussed initialization, access modifiers, and core object-oriented programming principles. We use initializers to set up an instance by configuring properties or other setup. Access modifiers are used to restrict access to properties and methods in our Swift code and led to our discussion about encapsulation, or data hiding. Encapsulation is an OOP principle that allows safe access to our internal state. Inheritance is another principle that allows us to create hierarchies of classes through a super/subclass relationship. Finally, we discussed polymorphism, which allows us to use abstractions so we don’t necessarily know or care about the underlying details of an implementation.