Composition vs. Inheritance: code architecture solutions explained in Swift

Composition and inheritance are both fundamental programming techniques when working in object-oriented programming languages. You’ve likely been using both patterns in your code already, even though you might not know what they mean.

Over the past ten years, I’ve often been using inheritance throughout my code. Although using inheritance often worked out fine, I’ve got quite some examples in which I wished I’d used a different approach since the code became hard to maintain and test—reason enough for me to dive into the details around composition vs. inheritance.

What is Inheritance?

Inheritance means using the default method and property implementations of a superclass. You can describe inheritance as “subclassing a superclass.” A subclass may override specific properties or methods to alter the default behavior.

The most common example would be defining your custom UIViewController:

class BlogViewController: UIViewController {
    // Implementation details..
}

In this example, the BlogViewController inherits all functionality from the UIViewController default implementation. We can override the default implementation by using the override keyword before a method or property:

class BlogViewController: UIViewController {

    override var prefersStatusBarHidden: Bool {
        true
    }

    override func viewDidLoad() {
        // Reuse default implementation
        super.viewDidLoad()

        // Add custom logic:
        view.backgroundColor = .red
    }
}

You can see that we’re now always hiding the status bar, and we’re setting the view background color to red. We make sure to reuse the default implementation of the superclass viewDidLoad method by calling the method on super. Using the super accessor, you can access non-private default implementations of your superclass.

The risk of endless inheritance

Since inheritance is transitive, a class can inherit from another class that inherits from another class, and so on.

/// Inherits from `BlogViewController` which inherits from `UIViewController`
class SwiftLeeBlogViewController: BlogViewController {

    /// When the status bar is visible, it might not be easy to find the cause.
    override var prefersStatusBarHidden: Bool {
        false
    }
}

In theory, you could be reusing a lot of default implementations under the hood, which might not be noticeable right away. Debugging and testing code can become hard since code becomes less visible and less accessible.

Structs don’t support inheritance

Since inheritance is about reusing default implementations from a superclass, we can’t use inheritance with structs. You could see protocol conformance with default implementations as a replacement of inheritance, but this is a different technique. On the other hand, structs work great with compositions and benefit from working with value types only.

What is Composition?

Composition comes down to combining multiple parts to create a new outcome. You can see an app using numerous frameworks as an outcome of composing frameworks together. We can define composition as an instance providing some or all of its functionality by using another object.

The most common example I’m using lately is implementing modern collection views using a compositional layout. The name itself already indicates being an example of composition in the default Apple APIs. Looking at the following code example, we can see the layout is composed by combining items, groups, and sections:

let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.2),
                                     heightDimension: .fractionalHeight(1.0))
let item = NSCollectionLayoutItem(layoutSize: itemSize)

let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),
                                      heightDimension: .fractionalWidth(0.2))
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize,
                                                 subitems: [item])

let section = NSCollectionLayoutSection(group: group)
let layout = UICollectionViewCompositionalLayout(section: section)

The benefit of using structs and enums with composition

Unlike with inheritance, we can use composition easily in combination with value types like structs and enums. Like explained in my article Struct vs. classes in Swift: The differences explained, value types have a lot of benefits like performance and memory safety. Since inheritance requires subclassing, we can only use it in combination with classes.

Inheritance vs Composition: The differences explained

To better describe the differences between inheritance and composition, it’s good to use a code example.

Take the following example of a label decorator in which we want to have a bold red label as an outcome. The code looks as follows if we use inheritance:

class RedLabelDecorator {
    func decorate(_ label: UILabel) {
        label.textColor = .red
    }
}

class BoldRedLabelDecorator: RedLabelDecorator {
    override func decorate(_ label: UILabel) {
        super.decorate(label)
        label.font = .boldSystemFont(ofSize: label.font.pointSize)
    }
}

In this case, using inheritance comes with a few downsides:

  • We need to use reference type classes
  • BoldRedLabelDecorator inherits from RedLabelDecorator and can be passed into a method that expects a RedLabelDecorator only, potentially causing side-effects for that method
  • There’s a missing potential of creating single responsibility decorators, allowing to reuse them in other places. For example, a bold decorator that can be reused in multiple places.

To better explain these differences, we can look at the same code example written with composition.

struct RedLabelDecorator {
    func decorate(_ label: UILabel) {
        label.textColor = .red
    }
}

struct BoldLabelDecorator {
    func decorate(_ label: UILabel) {
        label.font = .boldSystemFont(ofSize: label.font.pointSize)
    }
}

struct BoldRedLabelDecorator {
    func decorate(_ label: UILabel) {
        RedLabelDecorator().decorate(label)
        BoldLabelDecorator().decorate(label)
    }
}

The outcome will be the same as the inheritance example, but we’re using composition which comes with the following benefits:

  • Structs allow better performance and memory safety
  • Both bold and red label decorator are reusable for other decorators
  • Each decorator can be tested in isolation

Of course, we can optimize this code by making use of a Decorator protocol, for example.

Can I combine Composition with Inheritance?

I believe there’s no one-way solution. In my projects, I’m often combining many patterns like MVVM, MVC, and Composition and Inheritance. I always aim to create testable, reusable code that’s easy to understand.

Funny enough, even though I’m writing articles for five years and developing apps for over ten years, composition was still not on top of my mind when writing code solutions. Looking back at code examples in my projects that caused the most trouble, I could argue that composition would have led to better maintainable code.

Conclusion

Both inheritance and composition are often used in Apple’s SDKs and require us to think with a different mindset. Composition has the benefit of using value types and often results in better reusable code. Though, inheritance might sometimes be inevitable, which you shouldn’t see as a bad thing. You can combine both patterns in a single project, and one should pick the right solution that fits the problem best.

If you like to learn more tips on Swift, check out the swift category page. Feel free to contact me or tweet me on Twitter if you have any additional suggestions or feedback.

Thanks!