Swift: OOP concepts
Learn OOP with Swift. A comprehensive guide by ChatGPT.
What is OOP?
Object-Oriented Programming (OOP) is a programming paradigm based on the concept of "objects", which can contain data and code: data in the form of fields (attributes), and code, in the form of procedures (methods).
Some key concepts of OOP are:
Classes - A template that defines the properties and behaviors that objects of a certain kind have. For example, a Dog class defines what makes a dog, a dog.
Objects - Instances of a class. For example, Spot is an object instantiated from the Dog class.
Inheritance - The ability for one class to inherit the properties and behaviors of another class. This allows for code reuse and establishes an is-a relationship. For example, a Dalmatian class can inherit from the Dog class.
Encapsulation - The bundling of data with the methods that operate on that data. This allows objects to hide their internal representation.
Polymorphism - The ability for objects of different types to respond to method calls of the same name, each in its own way. This allows for code reuse and flexibility.
OOP helps us model real-world entities as software objects which interact by method calls. This makes the code more modular, reusable, and easier to maintain and debug.
So in summary, OOP organizes software design around objects rather than "functions and logic" like procedural programming. OOP models real world entities more effectively.
Multi-Paradigm Approach
Pure OOP languages like Java and C++ have some limitations:
Inflexible - Objects are tightly coupled and have rigid hierarchies. It's hard to reuse code across hierarchies.
Verbose - The class and object syntax can be verbose and boilerplate-heavy.
Imperative - The focus is on how to get things done rather than what needs to be done. This leads to mutable state and side effects.
Complex - The object model can become complex with many layers of inheritance and polymorphism.
Functional programming aims to overcome these limitations by focusing on:
Declarative code - Expressing what needs to be done rather than how.
Immutability - Avoiding mutable state and side effects.
Simple data transformation - Using functions that take input and produce output.
Swift combines OOP and FP in a pragmatic way:
It has classes, inheritance and polymorphism like an OOP language.
But it also has features like:
First-class functions
Closures
Higher-order functions
Optionals
Immutable value types
This allows Swift code to be:
Declarative - Functions are used to transform and filter collections.
Concise - Functions can be passed around as arguments.
Reusable - Functions can be reused in different contexts.
Safe - Optionals and pattern matching handle nil values gracefully.
So Swift combines the best parts of OOP (abstraction and reuse) with the best parts of FP (immutability and simplicity) to create a very pragmatic and enjoyable programming language. The FP features help mitigate some of the issues with a pure OOP approach.
Class Syntax
Here is how you declare a class and instantiate objects in Swift:
// Define a class
class Person {
// Properties
var name: String
var age: Int = 0
// Initializer
init(name: String) {
self.name = name
}
// Methods
func printName() {
print(self.name)
}
}
// Instantiate objects
let john = Person(name: "John")
let jane = Person(name: "Jane")
// Access properties
print(john.name) // Prints John
print(jane.age) // Prints 0 (default value)
// Call methods
john.printName() // Prints John
jane.printName() // Prints Jane
Comments:
We define a
Person
class with aname
string property and anage
integer property with a default value of 0.We define an initializer
init(name: String)
to initialize a person's name when creating an object.We define a
printName()
method to print the person's name.We instantiate 2 objects -
john
andjane
- by calling the initializer and passing a name string.We access the properties and call the method on each object to demonstrate they are independent instances of the
Person
class.Swift classes and structs are similar, but the difference is that structs are value types while classes are reference types. This means structs are copied when assigned while classes are referenced.
When you need a small record data type you use a struct. When you need a complex data type that has fields but also methods, you should use classes.
Initializers
In Swift, constructors are used to initialize instances of a class. They are called initializers.
An initializer has the following properties:
It has the same name as the class.
It is used to initialize the properties of an instance when it is created.
It allows us to ensure that an instance is properly initialized before it is used.
For example:
class Person {
var name: String
init(name: String) {
self.name = name
}
}
let john = Person(name: "John")
let jane = Person(name: "Jane")
Here we have a Person
class with an initializer init(name: String)
. When we instantiate objects using let john = Person(name: "John")
, the initializer is called to initialize the name
property and ensure the instance is properly initialized.
The instantiation process works as follows:
When we call
Person(name: "John")
, the initializerinit(name: String)
is invoked.The initializer initializes the instance by setting the
name
property to the passed argument "John".The initializer returns the newly created instance, which is assigned to the
john
constant.The instance is now properly initialized and can be used.
So in summary:
Initializers initialize an instance when it is created.
They ensure the instance is properly initialized before it can be used.
They are called automatically whenever we instantiate an instance from a class.
Note: Observe in Swift we do not need to use "new" keyword like in Java that I always forget to use and the program just crasjes without telling me what I miss. Also in Scala, this keyword is removed. Good choice Swift.
Inheritance Model
Swift is using single inheritance model, similar to Java. In Swift, the root class is Foundation.Object. All other classes inherit from this root class.
Inheritance allows one class to inherit the properties and methods of another class. It represents an "is-a" relationship between classes.
The syntax for inheritance in Swift is:
class Subclass: Superclass {
// subclass properties and methods
}
For example:
class Vehicle: Foundation.Object {
var numberOfWheels = 0
}
class Car: Vehicle {
var numberOfDoors = 0
}
let car = Car()
car.numberOfWheels // Inherited from Vehicle class
car.numberOfDoors // Declared in Car class
Here:
Vehicle
is the superclassCar
is the subclass that inherits fromVehicle
Car
inherits thenumberOfWheels
property fromVehicle
Car
also declares its ownnumberOfDoors
property
So in this example:
Foundation.Object
is the root classVehicle
inherits from the root classCar
inherits fromVehicle
The benefits of inheritance are:
Code reuse: subclasses inherit existing properties and methods.
Polymorphism: subclasses can override inherited properties and methods.
Represents a real-world "is-a" relationship between classes.
Abstraction
Abstraction is the concept of hiding unnecessary details and exposing only relevant information.
In Swift, abstraction is achieved through:
- Classes:
A class exposes only essential properties and methods while hiding implementation details.
For example:
class Vehicle {
var numberOfWheels = 0
func drive() {
// Implementation details hidden
// ...
}
}
The Vehicle
class abstracts away how driving is actually implemented. It only exposes the drive()
method.
- Access modifiers:
Using access modifiers like private
and internal
allows hiding certain details while exposing others.
For example:
class Vehicle {
private var make = "Ford" // Hidden from outside the class
internal var numberOfWheels = 0 // Exposed to this file
}
make
is hidden from outside the class, while numberOfWheels
is exposed to other classes in the same file.
- Protocols:
Protocols define a contract without providing implementation. Classes that conform to the protocol implement the requirements.
For example:
protocol Drivable {
func drive()
}
class Car: Drivable {
func drive() {
// Implementation
}
}
The Drivable
protocol abstracts away the implementation of drive()
. Any class that conforms to it must provide an implementation.
So in summary, abstraction in Swift is achieved through:
Classes that expose only essential properties and methods
Access modifiers that hide certain details
Protocols that define a contract without implementation
This allows modeling real-world entities in an abstract way while hiding unnecessary details.
Notes for Java developers:
Swift does not have explicit support for abstract classes or traits. However, it provides protocols and extensions which serve a similar purpose.
Abstract classes: In object-oriented languages like Java and C++, abstract classes are used to provide partially implemented functionality that subclasses must complete. Abstract classes cannot be instantiated directly - they must be subclassed.
Swift does not have the concept of "abstract classes". However, protocols serve a similar purpose:
Protocols define a blueprint of requirements that conforming types must implement.
Protocols cannot be instantiated directly, only types that conform to them can be instantiated.
For example, in Java we may have:
abstract class Vehicle {
abstract void drive();
}
class Car extends Vehicle {
void drive() {
// Implementation
}
}
In Swift, we achieve the same thing with a protocol:
protocol Drivable {
func drive()
}
class Car: Drivable {
func drive() {
// Implementation
}
}
So protocols serve as an "abstract class-like" mechanism in Swift.
Traits: In languages like Scala, traits are used to define methods that can be mixed into classes. Traits are similar to interfaces but can also have implementation.
Swift does not have traits, but extensions serve a similar purpose:
Extensions can add new functionality to an existing class without modifying that class.
Extensions can provide method and property implementations.
Hope this helps Java and Scala developers to learn the differences. In my oppinion, Swift has done a good job making a cleaner syntax easy to grasp. One more point for Swift.
Polymorphism
Polymorphism in Swift is implemented through:
- Inheritance: When a subclass inherits from a superclass, it can override the superclass's methods. This allows the subclass to have a different implementation of that method.
For example:
class Vehicle {
func makeSound() {
print("Vroom!")
}
}
class Car: Vehicle {
override func makeSound() {
print("Honk Honk!")
}
}
let vehicle = Vehicle()
vehicle.makeSound() // Vroom!
let car = Car()
car.makeSound() // Honk Honk!
Here, the Car
subclass has overridden the makeSound()
method to have its own implementation. So polymorphism is achieved - the same method behaves differently based on the subclass.
- Protocols: When multiple types conform to the same protocol, they can be treated polymorphically.
For example:
protocol Drivable {
func drive()
}
class Car: Drivable {
func drive() {
// Car driving implementation
}
}
class Bike: Drivable {
func drive() {
// Bike driving implementation
}
}
func useVehicle(_ vehicle: Drivable) {
vehicle.drive()
}
let car = Car()
useVehicle(car)
let bike = Bike()
useVehicle(bike)
The useVehicle()
function can call drive()
polymorphically on either a Car
or Bike
because they both conform to the Drivable
protocol.
- Generics: When working with generics, the generic type can have different implementations at runtime. This enables polymorphic behavior.
For example:
func print<T>(item: T) {
print(item)
}
print(item: "Hello")
print(item: 10)
The print()
function works polymorphically on different types - strings and integers.
So in summary, Swift implements polymorphism through:
Inheritance, where subclasses can override methods to provide their own implementation
Protocols, where conforming types can be used polymorphically
Generics, where the generic type can have different implementations at runtime
This allows writing flexible code that works with various types in Swift.
Encapsulation
Encapsulation is the bundling of data and methods that work on that data within one unit called a class. It hides the internal implementation of the data from the outside world.
In Swift, encapsulation is achieved through:
- Properties: Properties define the data/variables that are encapsulated within a class. They can be stored or computed properties.
For example:
class Person {
var name: String // Stored property
var age = 30 // Stored property with default value
var nickname: String { // Computed property
return "Bob"
}
}
The name
and age
properties store the actual data, while nickname
is a computed property that returns a hard coded value.
- Access modifiers: By default, properties and methods are internal (available within the same module). We can use access modifiers to restrict or widen access:
private: Only available within the current scope
fileprivate: Available within the current file
internal: Available within the current module (default)
public: Available everywhere
open: Available everywhere, and can be overridden by subclasses
For example:
class Person {
private var ssn: String // Social security number - only available within this class
public var name: String // Available everywhere
}
Here ssn
is private and encapsulated within the Person
class, while name
is public.
- Methods: Methods define the functionality that operates on the properties. They encapsulate the implementation.
For example:
class Person {
var name: String
func sayHello() {
print("Hello, my name is \(name)")
}
}
The sayHello()
method operates on the name
property, but the implementation is hidden from outside.
Best practices:
Define properties as private whenever possible. Make them public only if other classes need access.
Use access modifiers appropriately to hide implementation details while exposing essential functionality.
Encapsulate related data and methods within one class.
Prefer computed properties over stored properties if possible.
In summary, encapsulation in Swift is achieved by:
Defining properties to store data
Using access modifiers to control access
Defining methods that operate on the properties
Grouping related data and methods within classes
This allows hiding implementation details while exposing essential functionalities through a well-defined interface.
Disclaim: I have done my best to ask the right questions to learn OOP in Swift. I hope this helps. Most of the article is generated with Rix, I have just added some notes. If you like this article, comment about it and maybe this will help others to find it. Learn and prosper. ๐