Abstraction in Swift: A Comparative Look at Kotlin and Swift

Object-oriented programming (OOP) is built upon core principles such as abstraction, encapsulation, inheritance, and polymorphism. These concepts often surface in technical interviews and discussions with peers, yet their practical use can sometimes be elusive in real projects. During the development of 11 screens in one of my projects, I encountered repetitive code. Specifically, the same observe method for handling Kotlin Multiplatform’s StateFlow was reimplemented across several view models. This redundancy signaled the need for abstraction: by extracting common logic into a single module, we could both eliminate duplication and ease code maintenance. In this article, I’ll explore the concept of abstraction and demonstrate how it’s implemented in Kotlin and Swift, highlighting their similarities and differences along the way. Abstraction in Kotlin and Swift At its core, abstraction involves hiding implementation details while exposing only the necessary functionality. This separation simplifies interactions with objects and reduces code complexity. Abstraction in Kotlin In Kotlin, abstraction is achieved using abstract classes and interfaces. Abstract Classes: An abstract class can contain both abstract methods (methods without an implementation) and concrete methods (methods with an implementation). For example: abstract class AbstractClass { abstract fun abstractMethod() fun printThis() { println("Implementation") } } class ParentClass : AbstractClass() { override fun abstractMethod() { printThis() // Calls the already implemented method } } Key Rules for Abstract Classes in Kotlin: An abstract class in Kotlin is designed exclusively for inheritance and cannot be instantiated. It can contain both abstract methods (methods without an implementation) and concrete methods (methods with an implementation). Abstract classes are used to provide a common interface and implementation for their subclasses. When a subclass inherits from an abstract class, it must provide implementations for all abstract methods defined in the abstract class. If a subclass does not implement all abstract methods, it must also be declared as abstract. Interfaces Kotlin interfaces, on the other hand, serve as a contract that contains only abstract methods and cannot have a state (fields). For example: interface TestInterface { fun doIt() } class TestClass : TestInterface { override fun doIt() { println("Hello") } } Here, we defined TestInterface as a contract for TestClass, which requires now the implementation of doIt() method inside. A More Detailed Example: Consider a scenario where you have a Person abstract class with both abstract and concrete methods: // Abstract class abstract class Person(private val name: String, private var age: Int) { // Abstract method abstract fun birthDate(date: String) // Non-abstract method open fun personDetails() { println("Person's Name: $name") println("Age: $age") } } // Subclass inheriting from the abstract class class Employee(name: String, age: Int, private var salary: Double) : Person(name, age) { // Implementing the abstract method birthDate() override fun birthDate(date: String) { println("Birth Date: $date") } // Overriding the personDetails() method override fun personDetails() { // Calling the personDetails method from the abstract class super.personDetails() println("Salary: $salary") } } We create an abstract class Person and add a constant name and a variable age to the constructor. The name will remain unchanged, while the employee’s age will change in the future, so we define it as var. At the same time, we encapsulate these properties to restrict access to them only within the abstract class. The implementation of the birthDate method will vary depending on the subclass, so we use the abstract keyword to enforce its implementation in child classes. The personDetails() method already has a body, but we may want to extend it in subclasses. To allow this, we mark it with the open keyword. Additionally, we introduce an internal salary variable in the constructor of the child class and display the salary information after printing the basic employee details. Advantages and Disadvantages of Abstract Classes in Kotlin: Advantages Abstract classes allow defining common functionality that can be shared among multiple subclasses. They can contain both abstract methods, which must be implemented in subclasses, and concrete methods with implementations that can be overridden. However, in Kotlin (unlike Java), functions are final by default, so if you expect a method to be overridden, you must explicitly mark it as open. Abstract classes can maintain state through fields and properties that can be inherited and used by subcla

Feb 6, 2025 - 15:41
 0
Abstraction in Swift: A Comparative Look at Kotlin and Swift

Object-oriented programming (OOP) is built upon core principles such as abstraction, encapsulation, inheritance, and polymorphism. These concepts often surface in technical interviews and discussions with peers, yet their practical use can sometimes be elusive in real projects.

During the development of 11 screens in one of my projects, I encountered repetitive code. Specifically, the same observe method for handling Kotlin Multiplatform’s StateFlow was reimplemented across several view models. This redundancy signaled the need for abstraction: by extracting common logic into a single module, we could both eliminate duplication and ease code maintenance. In this article, I’ll explore the concept of abstraction and demonstrate how it’s implemented in Kotlin and Swift, highlighting their similarities and differences along the way.

Abstraction in Kotlin and Swift

At its core, abstraction involves hiding implementation details while exposing only the necessary functionality. This separation simplifies interactions with objects and reduces code complexity.

Abstraction in Kotlin

In Kotlin, abstraction is achieved using abstract classes and interfaces.

Abstract Classes:

An abstract class can contain both abstract methods (methods without an implementation) and concrete methods (methods with an implementation).

For example:

abstract class AbstractClass {
    abstract fun abstractMethod()

    fun printThis() {
        println("Implementation")
    }
}

class ParentClass : AbstractClass() {
    override fun abstractMethod() {
        printThis() // Calls the already implemented method
    }
}

Key Rules for Abstract Classes in Kotlin:

  1. An abstract class in Kotlin is designed exclusively for inheritance and cannot be instantiated.
  2. It can contain both abstract methods (methods without an implementation) and concrete methods (methods with an implementation).
  3. Abstract classes are used to provide a common interface and implementation for their subclasses.
  4. When a subclass inherits from an abstract class, it must provide implementations for all abstract methods defined in the abstract class. If a subclass does not implement all abstract methods, it must also be declared as abstract.
Interfaces

Kotlin interfaces, on the other hand, serve as a contract that contains only abstract methods and cannot have a state (fields). For example:

interface TestInterface {
    fun doIt()
}

class TestClass : TestInterface {
    override fun doIt() {
        println("Hello")
    }
}

Here, we defined TestInterface as a contract for TestClass, which requires now the implementation of doIt() method inside.

A More Detailed Example:
Consider a scenario where you have a Person abstract class with both abstract and concrete methods:

// Abstract class
abstract class Person(private val name: String, private var age: Int) {

    // Abstract method
    abstract fun birthDate(date: String)

    // Non-abstract method
    open fun personDetails() {
        println("Person's Name: $name")
        println("Age: $age")
    }
}

// Subclass inheriting from the abstract class
class Employee(name: String, age: Int, private var salary: Double) : Person(name, age) {

    // Implementing the abstract method birthDate()
    override fun birthDate(date: String) {
        println("Birth Date: $date")
    }

    // Overriding the personDetails() method
    override fun personDetails() {
        // Calling the personDetails method from the abstract class
        super.personDetails()
        println("Salary: $salary")
    }
}

We create an abstract class Person and add a constant name and a variable age to the constructor. The name will remain unchanged, while the employee’s age will change in the future, so we define it as var. At the same time, we encapsulate these properties to restrict access to them only within the abstract class.

The implementation of the birthDate method will vary depending on the subclass, so we use the abstract keyword to enforce its implementation in child classes. The personDetails() method already has a body, but we may want to extend it in subclasses. To allow this, we mark it with the open keyword. Additionally, we introduce an internal salary variable in the constructor of the child class and display the salary information after printing the basic employee details.

Advantages and Disadvantages of Abstract Classes in Kotlin:
Advantages
  • Abstract classes allow defining common functionality that can be shared among multiple subclasses.
  • They can contain both abstract methods, which must be implemented in subclasses, and concrete methods with implementations that can be overridden. However, in Kotlin (unlike Java), functions are final by default, so if you expect a method to be overridden, you must explicitly mark it as open.
  • Abstract classes can maintain state through fields and properties that can be inherited and used by subclasses.
  • They serve as a contract for subclasses, defining which methods must be implemented.
Disadvantages
  • A class can inherit from only one abstract class, whereas it can implement multiple interfaces.
  • Abstract classes cannot be instantiated directly and are meant solely for inheritance.
When to Use What?
  • Use interfaces when you need to define a set of rules that classes must follow.
  • Use abstract classes when you need to define shared logic that subclasses can inherit and extend.

Abstraction in Swift

Unlike some other programming languages, such as Kotlin or Java, Swift does not have the concept of abstract classes. Instead, Swift follows an approach known as protocol-oriented programming (POP)

In Swift, abstraction is typically implemented using protocols. A protocol defines a set of methods and properties that a class, structure, or enumeration must implement. This allows us to establish a common structure for different types without concerning ourselves with their specific implementations.

Protocols

Protocols in Swift are similar to interfaces in other programming languages:

  • They define a set of methods and properties that any conforming type must implement.
  • Protocols can be adopted by classes, structures, and enumerations, making them a flexible tool.
  • Protocols can be extended to provide default implementations, making methods or properties “optional.” This means that types conforming to the protocol can either provide their own implementations or use the default ones (via an extension, as shown below).

For example, you can define a protocol called Drawable, which requires a type to implement a draw() method. Any class, structure, or enumeration that conforms to the Drawable protocol can be considered drawable. Additionally, protocols can inherit from other protocols, allowing you to build more complex abstractions from simpler ones.

Here’s an example of a protocol with a default method implementation inside an extension in Swift:

protocol Drawable {  
    func draw()
}

extension Drawable {  
    func draw() {  
        print("Default drawing")  
    }
}

class Circle: Drawable {  
    // We can provide a custom implementation of the method 
    // or use the existing one from the Drawable extension.
    func draw() {  
        print("Drawing a circle")  
    }
}

class Square: Drawable {  
    // This class will use the default implementation
    func test() {
        draw()
    }
}

In this example, Circle provides its own implementation of the draw() method, while Square uses the default implementation. Both types are considered Drawable, even though they provide different implementations of this protocol.

Protocol and Interface Extensions

Swift’s protocol extensions let you add method implementations that conforming types can use, similar to Kotlin’s default methods in interfaces. For example:

protocol TestProtocol {
    func doIt()
}

extension TestProtocol {
    func doIt() {
        print("Default implementation")
    }
}

class MyClass: TestProtocol {
    // If no implementation is provided here, MyClass uses the default from the extension.
}

One nuance in Swift is that if you implement a method in your class that’s also provided by a protocol extension, the compiler will use your class’s version without requiring an override keyword. This behaviour can sometimes reduce clarity because it isn’t immediately obvious that a default implementation exists.

Extending Interfaces and Protocols

In Kotlin, interfaces can be extended in a way similar to Swift:

interface Test {
    fun doIt()
}

// Extension function for the Test interface
fun Test.make() {
    doIt()
}

class TestClass : Test {
    override fun doIt() {
        print("Hello")
    }

    fun method() {
        make() // Outputs "Hello"
    }
}

In this example, we first declare an interface with the doIt() method and then define an extension function make() on the interface. This function simply calls doIt() from the interface.

However, it’s important to note that the class must implement the interface in order to call make(). This means all required methods, in this case, doIt(), must be implemented in the class.

Starting from Java 8, interfaces can include method implementations using the default keyword:

public interface MyInterface {  
    // Declaring a default method  
    default void defaultMethod() {  
        // Default method implementation  
    }  
}

In Kotlin, there is no need to use the default keyword, making the syntax simpler:

interface TestInterface {
    fun doIt() {
        println("Hello")
    }
}

class TestClass: TestInterface {
    fun makeIt() {
        doIt()
    } 
}

However, it is important to note that writing implementations inside interfaces should generally be avoided, as interfaces are meant to define contracts, not to provide specific implementations. This is the fundamental idea behind interfaces in object-oriented programming.

Inheritance in Swift

Now, let’s take a look at how to create a default implementation in Swift protocols.

First, we define a protocol TestProtocol with the doIt() function:

protocol TestProtocol {  
    func doIt()  
}

Now, if we implement a class that conforms to this protocol, we must define this function with its own implementation inside the class. This is straightforward.
However, since Swift protocols are extendable, we can move the implementation outside the class by using an extension:

extension TestProtocol {  
    func doIt() {  
        // Default implementation  
    }  
}

While writing this code, you may notice that there is no autocomplete for doIt(), and even more—Xcode did not initially show an error if a class failed to implement all required protocol methods. It was only with Xcode 9 (2017, during the era of watchOS 4 and iOS 11) that an error message was introduced for cases where a class does not provide implementations for all protocol methods.

It turns out that in Swift (or rather, in Xcode, as we’ll see later), we are not exactly implementing a method from the protocol. Instead, we are “tracing over the original design like a stencil,” rather than directly inheriting it.

Lack of code-autocomplete in protocol extension

Lack of code-autocomplete in protocol extension

However, in AppCode by JetBrains, when writing the same code, autocomplete is available:
Screenshot of AppCode IDE

Screenshot of AppCode IDE

In an extension, you can also freely define a new method with an implementation without declaring it in the protocol:

protocol TestProtocol {
    func doIt()
}

extension TestProtocol {
    func makeIt() {
        print("New method implementation")
    }
}

When attempting to provide a custom implementation in a subclass, despite the existence of a default method, we will also notice the absence of autocomplete:

Absence of autocomplete

Here, there will be no override keywords and no autocompletions.

We can use the existing implementation, but if we want to write our own, we must precisely match the function’s name and signature. Otherwise, we may encounter issues such as code duplication or calling the wrong method:
Here, the method makeIt() uses the implementation defined within the class.

Here, the method makeIt() uses the implementation defined within the class

Only by doing it exactly this way will we replace the function with our own implementation, discarding makeIt() from the protocol extension. If we mistype even a single character and fail to verify which function is actually being called, code duplication may occur.

Going back to Kotlin, if we want to write our own implementation of a method despite an existing one in the interface, in addition to autocomplete, we get the following behavior:

interface TestInterface {
    fun doIt() {
        println("Hello")
    }
}

class TestClass : TestInterface {
    override fun doIt() {
        super.doIt() // This calls the method defined in the interface
        // Here, we can add our own implementation:
        print("Bye!")
    }
}

By simply typing doIt() and using autocomplete, we not only eliminate potential errors, but also automatically include a super.doIt() call within our override as a bonus!

However, an exception to this rule is interface extensions in Kotlin:

Same lack of autocomplete

Here, we have written an extension for the testInt interface with the method makeIt(). If we insert it in place of TODO, the call will be successful. However, autocomplete for this method is not available inside TestClass. Instead, we can create a new method with the same name, and the compiler will then determine which makeIt() implementation to use inside TestClass.

At this point, the following questions arise regarding Swift and Xcode:

  1. If we declare a function with the same name inside a class, which function will be called? How does the compiler determine which function implementation to use?
  2. Where is the override keyword before func, as in class inheritance?
protocol TestProtocol {
    func doIt()
}

extension TestProtocol {
    func doIt() {
        // Will this one be called?
    }
}

class ViewController: UIViewController, TestProtocol {
    func doIt() {
        // Or will this one be called?
    }
}

Answering the Newly Raised Questions:

  1. If doIt() is implemented inside the class, that version will be called. Otherwise, the function from the protocol extension will be used. The compiler first checks for an implementation inside the class, and if it doesn’t find one, it falls back to the protocol extension.

In Swift, when working with protocols, there is a mechanism called the Witness Table. Essentially, it is a data structure that maps each method or property requirement in a protocol to its corresponding implementation in the conforming type.

For example, let’s consider two protocols and two classes, along with their mappings:

protocol FirstProtocol {
    func doIt()
}

protocol SecondProtocol {
    func makeIt()
}

class OurClass: FirstProtocol, SecondProtocol {
    func doIt() {
        // Implementation is required
    }

    func makeIt() {
        // Implementation of the method from the second protocol
    }
}

class SecondClass: FirstProtocol {
    func doIt() {
        // Implementation is required here as well
    }
}

Witness Table representation

Witness Table representation

Please note the bolded memory addresses for the methods in the two classes. The address for FirstProtocol remains the same across different implementations.

This means that for each protocol adopted by a class, a separate table is created, mapping the protocol’s methods to their respective implementations. Even though the protocols themselves remain identical, their method implementations differ across conforming classes.

Thus, we can conclude that:

Every method defined in a protocol must either be explicitly implemented or have a default implementation provided in a protocol extension.

In the earlier example, when we defined our own makeIt() method inside the class, the compiler first checked whether makeIt() existed within the class itself. Since it found the method, it did not associate it with the protocol, and static dispatching was more likely to be used instead of the Witness Table.

However, it is also important to note that if a method is implemented in a protocol extension, it is most likely to be handled via static dispatching as well.

Where is override?

Now, let’s move on to the second question: Where is the override keyword before func, as in classes?

In Swift, the override keyword is used when overriding a method or property from a parent class in a subclass. This keyword ensures that the parent class actually contains the method that the subclass is attempting to override.

However, in the context of protocol extensions, when we provide a “default implementation” for a protocol method in an extension, overriding this method inside a conforming class does not require the override keyword.

This can be subjectively problematic for code readability because without the override keyword, it becomes harder to determine:

  • Whether a method has a default implementation in a protocol extension.
  • Whether the method belongs to a protocol at all.

Inheritance

Now, this smoothly brings us back to the topic of abstract classes, which we previously explored using Kotlin as an example.

One of the fundamental principles of OOP is Inheritance. A classic example is the Car parent class, with Motorcycle and Truck as child classes.

All of these classes share common functionality, such as driving or honking, inherited from the Car class. At the same time, they each have their own unique methods and properties, depending on their characteristics.

For example:

  • A motorcycle cannot function as a dump truck.
  • A truck cannot easily maneuver through traffic like a motorcycle... at least theoretically..

Let’s assume we have a parent class and add two arbitrary functions to it:

class ParentClass {  
    func eat() {  
        print("Eating")  
    }  

    func drink() {  
        print("Drinking")  
    }  
}

As we already know, we can now create child classes that inherit from the parent class:

class ChildClassA: ParentClass {}

class ChildClassB: ParentClass {}  

At this point, A and B classes look identical, but now we can add new independent methods and properties to the child classes:

class ChildClassA: ParentClass {  
    func playGames() {}  
}

class ChildClassB: ParentClass {  
    func buyFood() {}  
}

In addition to adding new properties or methods, we also have the option to override existing ones that were declared in the parent class:

class ParentClass {  
    func eat() {  
        print("Eating")  
    }  
}

class ChildClassA: ParentClass {  
    override func eat() {  
        print("NOT EATING")  
    }  
}

To prevent accidental method overrides in a subclass, we can add the final keyword before the function:

class ParentClass {  
    final func eat() {  
        print("Eating")  
    }  
}

class ChildClassA: ParentClass {  
    override func eat() {  
        // Error  
    }  
}

The final keyword prevents the eat() method from being overridden in any subclass. Attempting to override it will result in a compiler error.

Additionally, final can also be placed before class in the parent class, completely prohibiting inheritance:

final class ParentClass {  
    func eat() {  
        print("Eating")  
    }  
}

// This will cause an error:
class ChildClassA: ParentClass {}  

Thus, based on the principles of inheritance and polymorphism, we can create our own abstract class (even without the abstract keyword) by providing implementations for common methods that will be used in the child classes.

Unlike abstract class in Kotlin, we won’t have all its features, such as the inability to instantiate an abstract class. However, implementing abstract classes in Swift still allows us to define a common structure and behavior for subclasses, while also enabling unique implementations in each subclass as needed.

Summarising the First Part: Inheritance Limitations in Swift

  1. Restriction on Multiple Inheritance Unlike some other programming languages, such as C++ or Python, Swift does not support multiple inheritance for classes. This means that a class in Swift can inherit from only one parent class:
class ParentClass {}  
class SecondParentClass {}  

class FirstSubclass: ParentClass, SecondParentClass {  
    // Error  
}
  1. Structures and Enumerations Cannot Inherit from Classes or Each Other In Swift, structs and enums cannot inherit from classes or other structs/enums. This is an intentional language restriction to ensure type safety and simplify inheritance:
class ParentClass {}  

struct SubStruct: ParentClass {  
    // Error  
}
  1. Restriction on Inheriting from Protocols In Swift, classes, structures, and enumerations can conform to multiple protocols, but they cannot inherit method implementations from these protocols. Instead, every method defined in a protocol must either:
  2. Be explicitly implemented in the conforming type, or
  3. Have a default implementation provided in a protocol extension.

  4. Restriction on Method Overriding
    In Swift, methods defined in a superclass can be overridden in a subclass using the override keyword. However, a subclass cannot override methods that were defined in a protocol extension.

protocol TestProtocol {
    func method()
}

extension TestProtocol {
    func method() {
        print("Printing it")
    }
}

class ParentClass: TestProtocol {
    // Inherits the default implementation from the protocol extension
}

class SubClass: ParentClass {
    override func method() {
        // Error: Method does not override any method from its superclass
    }
}

Of course, we could explicitly define method() inside ParentClass, but that would be a direct implementation, not an override...

Conclusion

I hope this section has helped clarify the theoretical aspects of inheritance, method overriding, and working with protocols in Swift and interfaces in Kotlin.

In this article, we explored interfaces in Kotlin and protocols in Swift, examined some key differences and nuances, discussed inheritance, and reviewed various ways to implement abstraction in both programming languages.

Additional Resources:

  1. Speed Difference Between Static Dispatch and Dynamic Dispatch
  2. More About Interfaces in Kotlin and Potential Nuances

Software versions I used for this article (as of December 2023):

  • macOS Sonoma 14.1.1 (23B81)
  • Xcode Version 15.1 (15C65) (and earlier versions starting with 15.0 RC)
  • Android Studio Hedgehog | 2023.1.1
  • Android Studio Giraffe | 2022.3.1 Patch 2
  • AppCode 2023.1.4