top of page

Chapter 4: Object-Oriented Programming

  • Writer: arda doğantemur
    arda doğantemur
  • May 5, 2023
  • 6 min read

Updated: May 6, 2023


ree

Object-oriented programming (OOP) is a programming paradigm that organizes code into objects, which contain data and methods. In Swift, classes and structures are used to define objects.

The key concepts of OOP in Swift are:

  • Encapsulation: bundling data and methods together into a single unit, and controlling access to that unit through public and private interfaces.

  • Inheritance: creating new classes that are modified versions of existing classes, and sharing code between them.

  • Polymorphism: defining methods in a generic way that can be applied to different types of objects, allowing for more flexible and reusable code.

Let's explore each of these concepts in more detail.


Classes and Structures

Classes and structures are used to define objects in Swift. They are similar in many ways, but have some key differences:

  • Classes are reference types, while structures are value types. This means that when you pass a class instance to a function or assign it to a variable, you are actually passing or assigning a reference to the object in memory. With structures, you are working with a copy of the data.

  • Classes can have inheritance, while structures cannot.

  • Classes can have deinitializers (code that is run when the object is deallocated), while structures cannot.

Here's an example of a class in Swift:

class Person {
    var name: Stringvar age: Intinit(name: String, age: Int) {
        self.name = name
        self.age = age
    }

    func sayHello() {
        print("Hello, my name is \(name) and I am \(age) years old.")
    }
}

This class represents a person with a name and an age. It has an initializer (init()) that sets the initial values of the name and age properties, and a method (sayHello()) that prints a greeting using the name and age properties.

You can create an instance of this class and call its methods like this:

let person = Person(name: "John", age: 30)
person.sayHello() // prints "Hello, my name is John and I am 30 years old."


Encapsulation

Encapsulation is the idea of bundling data and methods together into a single unit, and controlling access to that unit through public and private interfaces. This allows you to hide the implementation details of your code and prevent external code from modifying internal data in unexpected ways.

In Swift, you can use access control modifiers to specify the visibility of properties and methods. There are three levels of access control:

  • public: accessible from any source file in the same module or framework, as well as any source file that imports the module or framework.

  • internal: accessible from any source file in the same module or framework, but not from outside.

  • private: accessible only from within the same source file.

Here's an example of a class with public and private properties:

class BankAccount {
    public let accountNumber: Int
    private var balance: Double
    
    init(accountNumber: Int, initialBalance: Double) {
        self.accountNumber = accountNumber
        self.balance = initialBalance
    }

    public func deposit(amount: Double) {
        balance += amount
    }

    public func withdraw(amount: Double) -> Bool {
        if amount <= balance {
            balance -= amount
            return true
        } else {
            return false
        }
    }
}

This class represents a bank account with an account number and a balance. The accountNumber property is public, so it can be accessed from outside the class


Inheritance

Inheritance is the idea of creating new classes that are modified versions of existing classes, and sharing code between them. In Swift, you can define a new class that inherits from an existing class using the class keyword, followed by the name of the new class and the name of the class it is inheriting from, separated by a colon.

Here's an example of a class that inherits from the Person class we defined earlier:

class Student: Person {
    var studentID: Int
    init(name: String, age: Int, studentID: Int) {
        self.studentID = studentID
        super.init(name: name, age: age)
    }

    func study() {
        print("\(name) is studying.")
    }
}

This Student class has an additional property, studentID, and a new method, study(), but it also has all of the properties and methods of the Person class, thanks to inheritance.

You can create an instance of this class and call its methods like this:

let student = Student(name: "John", age: 20, studentID: 1234)
student.sayHello() // prints "Hello, my name is John and I am 20 years old."
student.study() // prints "John is studying."

Inheritance allows you to create new classes that are variations of existing classes, and reuse code without having to rewrite it. However, it can also lead to tightly coupled code and complex class hierarchies if used excessively, so it should be used judiciously.


Access Control

Access control is the mechanism in Swift that allows you to restrict the access of code to certain parts of your program. There are three levels of access control in Swift: public, internal, and private.

  • public: code that is marked as public can be accessed by any code that imports the module in which it is defined.

  • internal: code that is marked as internal can be accessed by any code within the same module. This is the default level of access control if none is specified.

  • private: code that is marked as private can only be accessed by code within the same file.

Here's an example:

public class PublicClass {
    internal var internalProperty: Int = 0
    private var privateProperty: String = "private"
    public init() {}

    internal func internalMethod() {}
    private func privateMethod() {}
}

In this example, PublicClass is marked as public, so it can be accessed from any code that imports this module. Its properties and methods are marked with various access levels to restrict their access.


Protocols and Delegates

Protocols are a way to define a set of methods and properties that a type must implement if it wants to conform to the protocol. Delegates are a design pattern that uses protocols to allow one object to communicate with another object.

Here's an example of a protocol:

protocol Printable {
    func printDocument()
    var numberOfPages: Int { get }
}

This protocol defines two requirements: a method called printDocument() and a property called numberOfPages. Any type that wants to conform to this protocol must implement these requirements.

Here's an example of a class that conforms to this protocol:

class Printer: Printable {
    var numberOfPages: Int = 0func printDocument() {
        print("Printing document...")
    }
}

This Printer class implements both requirements of the Printable protocol.


Extensions

Extensions allow you to add new functionality to existing types, including types defined by the system or by other modules. You can add new properties, methods, initializers, and even protocols to existing types using extensions.

Here's an example of an extension:

extension Int {
    func square() -> Int {
        return self * self
    }
}

This extension adds a new method called square() to the Int type, which returns the square of the integer. You can use this method on any integer value:

let number = 5
print(number.square()) // prints 25

Error Handling

Error handling is the mechanism in Swift that allows you to handle errors and recover from them in a structured way. In Swift, errors are represented by types that conform to the Error protocol.

Here's an example of a function that throws an error:

enum MathError: Error {
    case divideByZero
}

func divide(_ dividend: Int, by divisor: Int) throws -> Int {
    guard divisor != 0 else {
        throw MathError.divideByZero
    }
    return dividend / divisor
}

This divide(_:by:) function takes two integers as input and returns their quotient, but it can throw a MathError.divideByZero error if the divisor is zero. You can call this function within a do-catch block to handle the error:

do {
    let result = try divide(10, by: 2)
    print(result) // prints 5
}

Here's the complete code for that example:

Sure, here's the complete code for that example:

enum MathError: Error {
    case divideByZero
}

func divide(_ dividend: Int, by divisor: Int) throws -> Int {
    guard divisor != 0 else {
        throw MathError.divideByZero
    }
    return dividend / divisor
}

do {
    let result = try divide(10, by: 2)
    print(result) // prints 5
} catch MathError.divideByZero {
    print("Error: Divide by zero")
} catch {
    print("Error: \(error)")
}

In this example, we call the divide(_:_:) function with the arguments 10 and 2, which should return 5. We wrap this call in a do-catch block to handle any errors that might be thrown.

If the divide(_:_:) function throws a MathError.divideByZero error, we catch it and print a custom error message. If it throws any other error, we catch it and print the error message using the error parameter. In this chapter, we discussed the fundamentals of object-oriented programming (OOP) in Swift. We covered the concept of classes, objects, properties, methods, and inheritance. We also talked about how to create an object of a class, how to access its properties and methods, and how to override them in a subclass.

Understanding OOP is crucial for writing efficient and maintainable code. By encapsulating data and behavior within classes, we can create reusable and modular code that is easy to understand and extend.



Comments


© 2020 by Arda Doğantemur.

bottom of page