dev-resources.site
for different kinds of informations.
Object-Oriented Programming in Swift
Introduction
It has lovers and haters, but Object-Oriented Programming (OOP) is decidedly a subject that market expects you to master as a software engineer.
Theory
Literature says that it's a popular paradigm used in software development to organize code into modular and reusable objects. Swift, being an OOP language, supports four pillars of OOP: abstraction, inheritance, encapsulation, and polymorphism. In this post, we will try to explore these pillars:
Abstraction
Abstraction is the process of hiding implementation details and exposing only the "abilities" of the object. Think in Abstraction as not telling to the others how you do things, but instead what you are capable of doing.
Imagine that for cold starting a car, your car needs to turn on the battery, start the starter motor and then pump gas. It does all of these steps when you press the "Start" button.
Now, imagine that you'll represent this process in your code:
class Car {
var name: String
init(name: String) {
self.name = name
}
func start() {
turnBatteryOn()
startStarterMotor()
pumpFuel()
}
func turnBatteryOn() { /../ }
func startStarterMotor() { /../ }
func pumpFuel() { /../ }
}
let car = Car(name: "Ferrari")
car.start()
The Abstraction concept of OOP tells us that, whoever holds an instace of Car
shouldn't "know" that this object is capable of, for example, turning the battery on, or pumping fuel, since it's not relevant for the software requirements. What is relevant is that, the car can start.
In Swift, we can use protocols to achieve abstraction. A protocol defines a set of methods and properties that a class, struct, or enum can adopt without needing to specify how they work. This allows us to declare what actions an object can perform, without revealing how it does so. This is similar to interfaces in languages like Java or C#.
Let's take a look at an example:
// MARK: - Abstraction
protocol Vehicle {
var name: String { get set }
func start()
}
// MARK: - Concrete implementation
class Car: Vehicle {
var name: String
init(name: String) {
self.name = name
}
func start() {
turnBatteryOn()
startStarterMotor()
pumpFuel()
}
func turnBatteryOn() { /../ }
func startStarterMotor() { /../ }
func pumpFuel() { /../ }
}
// MARK: - Using the code
let car: Vehicle = Car(name: "Ferrari")
car.start()
// If you try this:
car.startStarterMotor()
// You'll receive: protocol 'Vehicle' has no instances of `startStarterMotor`.
// This ensures that only intended methods are accessible.
In the above code, we define a protocol Vehicle
that has a name
property and a start()
method. We then create a Car
class that conforms to the Vehicle
protocol. The Car
class has a name property and implements the start()
method. We can now create instances of the Car
class and call the start()
method on them.
If we have now other types of Vehicle
s, that starts in a different way, our code that expects a Vehicle
object is already compatible:
class Boat: Vehicle {
var name: String
init(name: String) {
self.name = name
}
func start() {
turnBatteryOn()
// Look that this boat doesn't have a starter motor. You need to manually pull a rope to start it, but this doesn't matter to whoever is using the object.
pullStarterRope()
pumpFuel()
}
func turnBatteryOn() { /../ }
func pullStarterRope() { /../ }
func pumpFuel() { /../ }
}
let boat: Vehicle = Boat(name: "Fishing Boat")
// Just start the same way it'd start a car
boat.start()
Inheritance
Maybe the most known OOP principle, inheritance is pretty straight-forward: children inherits behaviors from parents.
To understand it better, we need to bring what a child
, a parent
and a behavior
is in a software code context. In Swift, children and parents are object types, more specifically Classes
. In this language it's not possible to do inheritance between struct
objects.
Behaviors
are the properties and/or the functions of these classes. Let's see an example:
class Pet {
var name: String
init(name: String) {
self.name = name
}
func eatFood() {
print("The pet is eating the food")
}
}
class Dog: Pet {
}
class Cat: Pet {
}
let dog = Dog(name: "Joey")
dog.eatFood()
let cat = Cat(name: "Phoebe")
cat.eatFood()
In our app, we have Dog
s and Cat
s. Since both of them share some behaviors (as having a name and eating food) we can use the concept of inheritance to improve our code, writting the logic only one time and sharing with them. If we have a bug inside our eatFood()
function, we have only one place to fix it for the entire app. That's the main benefit of using inheritance in your code.
The down-side
Even though it's considered one of the best practices of OOP, using inheritance can bring problems to your development cycle.
The concept leads to a tight coupling in your code, making it less flexible, and flexibility is something very important during software development, because softwares are not static things, but instead "living" beings that should be able to change due time.
Imagine having a Car
type in your code that consumes fuel
. You have also other types of Vehicle
s that also consumes fuel, so you decide to use inheritance to obtain the benefits of avoid repetition:
class Vehicle {
var speed: Double = 0.0
var fuelLevel: Double = 0.0
func fillTank() {
fuelLevel += 10
}
func throttle() {
if fuelLevel > 0 {
speed += 10
fuelLevel -= 5
} else {
print("Out of fuel!")
}
}
}
class Car: Vehicle {
}
class Motorcycle: Vehicle {
}
That works perfectly, in 2012 when you wrote this piece of code. But now your app should also support hybrid cars, that consumes fuel and electricity. If you update your Vehicle.throttle
function to support electricity, it will affect all the other types that shouldn't support it. So now, you have a huge work to do in order to support it.
A better approach would be using the concept of composition instead of inheritance for this case, but that's a subject for another post.
Encapsulation
Encapsulation is the process of hiding the implementation details of an object from the outside world. But differently from the abstraction, that says that you have to create abstract types and make your code depend on it, encapsulation is more about the concrete types (classes and structs). So, in order to "hide" things in concrete types, you need to use the language access control modifiers.
Swift’s access control modifiers are essential for encapsulation, limiting access to properties and methods based on scope:
-
public
: Accessible from any module. -
internal
(default): Accessible within the same module. -
fileprivate
: Accessible within the same file. -
private
: Only accessible within the same class or struct.
To makes things easier and learning OOP concepts only, let's consider only private
and internal
, being this last the default modifier (the one the compiler selects when you don't specify anything)
Here's an example:
class Person {
private var name: String
init(name: String) {
self.name = name
}
func greet() {
print("Hello, my name is \(name)")
}
}
let person = Person(name: "John")
person.greet() // Output: "Hello, my name is John"
person.name = "Mike" // Error: 'name' is inaccessible due to 'private' protection level
In the above code, we define a Person
class that has a private name
property and an internal greet()
method (we set it as internal by not specifying an accessor). We use the private access control modifier to make the name
property inaccessible from outside the Person
class scope. We can create an instance of the Person
class and call the greet()
method on it, but we cannot access the name
property directly.
That's important to guarantee that whoever holds an instance of your object, can do only things that they are supposed to.
Let's take a look in another example to make it clearer:
enum Rate {
static var hourlyRate: Double = 10
}
class Employee {
var workedHours: Double
private var salary: Double = 0
init(workedHours: Double) {
self.workedHours = workedHours
}
func calculateSalary() {
salary = workedHours * Rate.hourlyRate
}
func getSalary() -> Double {
return salary
}
}
let employee = Employee(workedHours: 40)
employee.calculateSalary()
print(employee.getSalary()) // ✅ Will display the correct salary based in the worked hours and rate
employee.workedHours = 48 // ✅ You can change how much the employee worked
employee.calculateSalary()
print(employee.getSalary()) // ✅ Displays correctly
employee.salary = 500 // 🚫 You'll receive "'salary' is inaccessible due to 'private' protection level". You cannot pass by the calculation logic
Polymorphism
Polymorphism means literally "many forms". In OOP, it's the ability of several objects of same type having totally different behaviors.
This concept could be confusing for beginners, you can find it sometimes similar to abstraction, and also to inheritance. And that's because you need to use both of these previous pillars to achieve polymorphism.
To implement Polymorphism, the commom type between different objects should be an abstraction. In Swift, we use protocols for that (in other languages you can use abstract classes also, but Swift doesn't have it), and also inheritance of classes to implement the different behaviors.
Let's see an example:
protocol Shape {
func area() -> Double
}
class Rectangle: Shape {
var width: Double
var height: Double
init(width: Double, height: Double) {
self.width = width
self.height = height
}
func area() -> Double {
return width * height
}
}
class Circle: Shape {
var radius: Double
init(radius: Double) {
self.radius = radius
}
func area() -> Double {
return Double.pi * radius * radius
}
}
// Here we have an array of `Shape`s. All the array elements are the same type
let shapes: [Shape] = [Rectangle(width: 10, height: 10), Circle(radius: 10)]
for shape in shapes {
// Here, the `shape` variable is always considered the type `Shape` by the compiler in all loops, but what the `area()` function does is different each time
print(shape.area())
}
This brings flexibility to the code, since you can create new specific types and pass it to every place that expects a Shape
.
Wrapping up
So recaping, Object-Oriented Programming (OOP) revolves around four key principles:
- Abstraction: Hides implementation details, exposing only what’s essential.
- Encapsulation: Protects internal state by restricting external access.
- Inheritance: Shares common behaviors across related types.
- Polymorphism: Allows objects to take on multiple forms, enhancing flexibility.
In a job interview or in your daily work, mastering OOP always is a good thing. As mentioned in the introduction, this set of concepts has lovers and haters. But in my entire career, all the engineers I met that stands against OOP are the ones that better understanding of its concepts. So studying it is not only a good idea, but mandatory in my opinion. And for the market, if you are an intermediate or senior professional, excelling it will be expected from you.
Featured ones: