Basic Object-oriented Principles
Before delving into Swift code, let’s first understand what it means to be object-oriented (OO) and the need for OO. We begin our discussion by talking about classes and objects.
BUILD GAMES
FINAL DAYS: Unlock 250+ coding courses, guided learning paths, help from expert mentors, and more.
In software, sometimes it is not sufficient to have loose methods and data structures floating around in our source code. We need some structure to group similar attributes and actions. Let’s look at an example.
Imagine we’re working in a factory that makes cars. We have several different models of cars that our assembly line produces from a blueprint. The machines along the assembly line are already programmed with the blueprint and perform the necessary actions to construct a functioning car at the end of the assembly line. When we’re making a multiple cars of the same model, the assembly line guarantees that the resulting cars are identical.
Suppose we wanted to model this assembly line in our Swift code. We might have something like the following.
var color = "Red" var numberOfDoors = 2 ...
But this code is ambiguous! Which car is red? Which car has 2 doors? If we wanted to make different cars, we would have to prefix each attribute a car has with the name of the different cars we wanted to create. This leads to a variable-naming and code quality nightmare!
Instead, we can bundle a car’s states and behaviors into a class. A state is just some attribute about the car, like color or number of doors. A behavior is an action that the car can perform, like drive forward or turn left. A class is just a blueprint for a software object or instance that bundles the states and behaviors. It allows us to represent a real-world thing, like a car, in our source code. We create an instance or object of a class when realize that blueprint. Think of it like constructing a new car after it has gone down the assembly line.
Let’s see some Swift code and create a class to represent a car.
class Car { var color = "Red" var numDoors = 4 var speed = 0 var gear = 1 var hasSunroof = false var sunroofOpen = false func stop() { speed = 0 } func increaseSpeed(_ speedToAdd: Int) { speed = speed + speedToAdd } func decreaseSpeed(_ speedToSub: Int) { speed = speed - speedToSub } func openSunroof() { if hasSunroof { sunroofOpen = true } } func closeSunroof() { if hasSunroof { sunroofOpen = false } } }
This class has states and behaviors that we used to represent a car. The states are called properties in Swift. They look like variable declarations except inside of a class. Behaviors are called methods in Swift and look like functions inside of the class declaration. We’ll be discussing them in more detail later.
Now that we’ve defined a blueprint for a car, we can create an instance, or instantiate, a new car similar to any variable in Swift.
var newCar = Car()
This new car will start with all of the default values of the properties, i.e. Red, 4 doors, etc. We can access and modify different properties on this instance by using the dot operator (.) after the variable name. For example, let’s make our car blue instead of red.
newCar.color = "Blue"
We used the variable name, dot operator, and property name to change the value of the property. We can do the same to access the property:
let newCarColor = newCar.color
We can call methods using the same dot operator and syntax, except we use the method name and type it like we’re calling a function.
newCar.increaseSpeed(25)
Now that we’ve seen how to use classes and instances, we’re going to delve more into properties and methods so we can build better object-oriented Swift code.
Properties
As we’ve seen before, properties represent the state of an object, and we can access the properties of an instance using the dot operator. Properties can be of any data type, even other classes! Consider a class representing a stick of RAM.
class RamStick { var totalRam = 2 }
Computers have slots that RAM sticks can fit into to give our computer more RAM! Let’s make a computer class with 4 slots for RAM.
class Computer { var ramSlot1 = RamStick() var ramSlot2 = RamStick() var ramSlot3 = RamStick() var ramSlot4 = RamStick() } var myComputer = Computer()
The properties in this class are actually instances of other classes! We have 4 RAM slots here, but it might be the case that we don’t use all of them all of the time! But what we’re doing here is wasting resources by forcing Swift to create 4 instances of RamStick for each instance of Computer that we create.
Instead, we can use lazy properties to tell Swift that we want to create the instance only if it’s being used. To declare a property to be lazy, we simply add the lazy modifier.
class Computer { var ramSlot1 = RamStick() lazy var ramSlot2 = RamStick() lazy var ramSlot3 = RamStick() lazy var ramSlot4 = RamStick() } var myComputer = Computer() myComputer.ramSlot1.totalRam = 4 myComputer.ramSlot2.totalRam = 4
Now when we create an instance, the only instance of RamStick created is ramSlot1. However, if we were to access a property on a lazy property, Swift will create the instance. In the above case, ramSlot3 and ramSlot4 are not created yet and may not ever be. This saves us some resources! Imagine making hundreds of computer instances in an array! All of those savings add up!
In addition to lazy properties, we can also have computed properties. These are properties whose values are computed from the values of other properties. For example, consider the total RAM of a computer: it is the sum of the all of the RAM in the RAM slots.
class Computer { var ramSlot1 = RamStick() lazy var ramSlot2 = RamStick() lazy var ramSlot3 = RamStick() lazy var ramSlot4 = RamStick() var totalRam: Int { get { return ramSlot1.totalRam + ramSlot2.totalRam + ramSlot3.totalRam + ramSlot4.totalRam } set { ramSlot1.totalRam = newValue } } }
We can write code that happens when we access or modify this property. When we access this property, we compute the total RAM from the RAM slots in the get block. When we set this property, we assign all of the RAM to the first RAM slot in the set block. The variable newValue is a context-specific variable that represents the new value we’re setting to, or the right-hand side of the assignment operator in other words. If we omit the set block, we can have a readonly computed property that we can access but not modify.
So far, we’ve only discussed instance properties, meaning that the values are tied to an instance, and each instance has its own values for each property. We can also have type properties whose values are bound to the type, not any particular instance. For example, suppose we wanted to keep track of all of the computers that were manufactured. We don’t want each instance to keep track of that because the numbers will all be different! Instead, the Computer type should keep track of this.
class Computer { static var numberOfComputers = 0 ... func manufacture() { ... Computer.numberOfComputers += 1 } ... } var computer1 = Computer() comptuer1.manufacture() Computer.numberOfComputers var computer2 = Computer() comptuer2.manufacture() Computer.numberOfComputers
We have a type property numberOfComputers declared with the static modifier. We don’t access it through a particular instance, but through the type name. The first time we print its value, we get 1b and the second time will be 2. This is because we can update it in the instance methods, but the value is tied to the type, not a particular instance. Whenever we manufacture a computer, we increment the total number of computers created. Always remember that type methods are bound to the type!
Methods
As we’ve seen before, methods represent the behavior of an object, and we can call methods on an instance using the dot operator. By behavior, we’re really asking the question “what can this object do?” Syntax and functionality-wise, methods operate very similarly to functions except they’re bundled into a class now.
Similar to properties, we can have instance methods and type methods. Instance methods are called on the instance, and type methods are called on the type. The rule with type methods is that we cannot access instance properties in a type method since we’re not calling it on an instance! Hence we can’t access any instance data, because there is no instance!
class Computer { static var numberOfComputers = 0 static func printTotalManufactured() { print(numberOfComputers) // OK to access since it is a type property! //print(totalRam) // NOT OK! totalRam is an instance property and relies on having an instance! } ... } Computer.printTotalManufactured()
Declaring a method to be a type method is similar to declaring property to be a type properties: add the static modifier! We call the type method on the type, similar to how we access type properties.
Methods without that static modifier are instance methods and require an instance. Instance methods also have access to a special, implicit variable called self. self refers to the current instance that we’re executing the method on. Usually, we use it to disambiguate between method parameters and instance properties. Consider this example.
class Computer { ... var isOn = false func changeStatus(_ isOn: Bool) { isOn = isOn } }
When we perform the assignment, are we assigning the property to the parameter or the parameter to the property? We use the self keyword to refer to the current instance so we can disambiguate.
class Computer { ... var isOn = false func changeStatus(_ isOn: Bool) { self.isOn = isOn } }
This code is unambiguous since self.isOn refers to the property on the current instance, which is the instance property isOn and not the parameter.
Since we use it to refer to the current instance, we cannot use the self property in a type method because we have no instance!
In this section, we discussed some basic object-oriented concepts like classes, properties, and methods. We discussed the difference between classes and instances. We learned about states and behaviors in the form of Swift properties and methods, respectively. We also took an in-depth approach to learning about properties and methods.