Swift: OOP concepts

Swift: OOP concepts

Learn OOP with Swift. A comprehensive guide by ChatGPT.

ยท

11 min read

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:

  1. 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.

  2. Objects - Instances of a class. For example, Spot is an object instantiated from the Dog class.

  3. 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.

  4. Encapsulation - The bundling of data with the methods that operate on that data. This allows objects to hide their internal representation.

  5. 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 a name string property and an age 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 and jane - 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:

  1. When we call Person(name: "John"), the initializer init(name: String) is invoked.

  2. The initializer initializes the instance by setting the name property to the passed argument "John".

  3. The initializer returns the newly created instance, which is assigned to the john constant.

  4. 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 superclass

  • Car is the subclass that inherits from Vehicle

  • Car inherits the numberOfWheels property from Vehicle

  • Car also declares its own numberOfDoors property

So in this example:

  • Foundation.Object is the root class

  • Vehicle inherits from the root class

  • Car inherits from Vehicle

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:

  1. 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.

  1. 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.

  1. 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:

  1. 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.

  1. 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.

  1. 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:

  1. 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.

  1. 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.

  1. 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. ๐Ÿ––

ย