Composing types in Swift
Composition is a super useful technique that lets us share code between multiple types in a more decoupled fashion. It's often posed as an alternative to subclassing, with phrases like "Composition over inheritance" - which is the idea of composing functionality from multiple individual pieces, rather than relying on an inheritance tree.
While subclassing/inheritance is also super useful (and Apple's frameworks, which we all depend on, rely heavily on that pattern), there are many situations in which using composition can let you write simpler and more robustly structured code.
This week, let's take a look at a few such situations and how composition can be used with structs, classes and enums in Swift.
Struct composition
Let's say that we're writing a social networking app, that has a User
model and a Friend
model. The user model is used for all kinds of users in our app, and the friend model contains the same data as the user one, but also adds new information - like on what date two users became friends.
When deciding how to set these models up, an initial idea (especially if you're coming from languages that traditionally have relied heavy on inheritance, like Objective-C) might be to make Friend
a subclass of User
that simply adds the additional data, like this:
class User {
var name: String
var age: Int
}
class Friend: User {
var friendshipDate: Date
}
While the above works, it has some downsides. Three in particular:
- To use inheritance, we are forced to use classes - which are reference types. For models, that means that we can easily introduce shared mutable state by accident. If one part of our code base mutates a model, it will automatically be reflected everywhere, which can lead to bugs if such changes are not observed & handled correctly.
- Since a
Friend
is also aUser
, aFriend
can be passed to functions that take aUser
instance. This might seem harmless, but it increases the risk of our code being used in "the wrong way", for example if aFriend
instance is passed to a function likesaveDataForCurrentUser
. - For a
Friend
, we don't actually want theUser
properties to be mutable (it's pretty hard to change the name of a friend 😅), but since we rely on inheritance we also inherit the mutability of all properties.
Let's use composition instead! Let's make User
and Friend
structs (which lets us take advantage of Swift's built in mutability features) and instead of making Friend
directly extend User
, let's compose a User
instance together with the new friend-specific data to form a Friend
type, like this:
struct User {
var name: String
var age: Int
}
struct Friend {
let user: User
var friendshipDate: Date
}
Note above how the user
property of a Friend
is a let
, meaning that it can't be mutated, even if a stand-alone User
instance can. Pretty neat! 👍
Class composition
So does that mean that we should make all of our types structs? I definitely don't think so. Classes are super powerful, and sometimes you want your types to have reference semantics rather than value semantics. But even if we choose to use classes, we can still use composition as an alternative to inheritance in many situations.
Let's build a UI that displays a list of some of our Friend
models from the previous example. We'll create a view controller - let's call it FriendListViewController
- that has a UITableView
to display our friends.
One very common way to implement a table view-based view controller is to have the view controller itself be the data source for its table view (it's even what UITableViewController
defaults to):
extension FriendListViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView,
numberOfRowsInSection section: Int) -> Int {
return friends.count
}
func tableView(_ tableView: UITableView,
cellForRowAt indexPath: IndexPath) -> UITableViewCell {
...
}
}
I'm not going to say that doing the above is bad and you should never do it, but if we're looking to make this functionality more decoupled and reusable - let's use composition instead.
We'll start by creating a dedicated data source object that conforms to UITableViewDataSource
, that we can simply assign our list of friends to and it'll feed the table view with the information that it needs:
class FriendListTableViewDataSource: NSObject, UITableViewDataSource {
var friends = [Friend]()
func tableView(_ tableView: UITableView,
numberOfRowsInSection section: Int) -> Int {
return friends.count
}
func tableView(_ tableView: UITableView,
cellForRowAt indexPath: IndexPath) -> UITableViewCell {
...
}
}
Then, in our view controller, we keep a reference to it and use it as the table view's data source:
class FriendListViewController: UITableViewController {
private let dataSource = FriendListTableViewDataSource()
override func viewDidLoad() {
super.viewDidLoad()
tableView.dataSource = dataSource
}
func render(_ friends: [Friend]) {
dataSource.friends = friends
tableView.reloadData()
}
}
The beauty of this approach is that it becomes super easy to reuse this functionality in case we want to display a list of friends somewhere else in our app (in a "Find a friend" UI for example). In general, moving things out of view controllers can be a good way to avoid the "Massive view controller" syndrome, and just like we made our data source a separate, composable type - we can do the same thing for other functionality as well (data & image loading, caching, and so on).
Another way to use composition with view controllers in particular is to use child view controllers. Check out "Using child view controllers as plugins in Swift" for more on that.
Enum composition
Finally, let's take a look at how composing enums can give us a more granular setup that can lead to less code duplication. Let's say that we're building an Operation
type that lets us perform some heavy work on a background thread. To be able to react to when an operation's state changes, we create a State
enum that has cases for when the operation is loading, failed or finished:
class Operation {
var state = State.loading
}
extension Operation {
enum State {
case loading
case failed(Error)
case finished
}
}
The above might look really straight forward - but let's now take a look at how we might use one of these operations to process an array of images in a view controller:
class ImageProcessingViewController: UIViewController {
func processImages(_ images: [UIImage]) {
// Create an operation that processes all images in
// the background, and either throws or succeeds.
let operation = Operation {
let processor = ImageProcessor()
try images.forEach(processor.process)
}
// We pass a closure as a state handler, and for each
// state we update the UI accordingly.
operation.startWithStateHandler { [weak self] state in
switch state {
case .loading:
self?.showActivityIndicatorIfNeeded()
case .failed(let error):
self?.cleanupCache()
self?.removeActivityIndicator()
self?.showErrorView(for: error)
case .finished:
self?.cleanupCache()
self?.removeActivityIndicator()
self?.showFinishedView()
}
}
}
}
At first glance it might not seem like anything is wrong with the above code, but if we take a closer look at how we handle the failed
and finished
cases, we can see that we have some code duplication here.
Code duplication is not always bad, but when it comes to handling different states like this it's usually a good idea to duplicate as little code as possible. Otherwise we'll have to write more tests and do more manual QA to test all possible code paths - and with more duplication it's easier for bugs to slip through the cracks when we're changing things.
This is another situation in which composition is super handy. Instead of just having a single enum, let's create two - one to hold our State
and another one to represent an Outcome
, like this:
extension Operation {
enum State {
case loading
case finished(Outcome)
}
enum Outcome {
case failed(Error)
case succeeded
}
}
With the above change in place, let's update our call site to take advantage of these composed enums:
operation.startWithStateHandler { [weak self] state in
switch state {
case .loading:
self?.showActivityIndicatorIfNeeded()
case .finished(let outcome):
// All common actions for both the success & failure
// outcome can now be moved into a single place.
self?.cleanupCache()
self?.removeActivityIndicator()
switch outcome {
case .failed(let error):
self?.showErrorView(for: error)
case .succeeded:
self?.showFinishedView()
}
}
}
As you can see, we have gotten rid of all code duplication and things are looking a lot more clear 👍.
Conclusion
Composition is a great tool that can lead to simpler, more focused types that are easier to maintain, reuse & test. While it's not a complete replacement for either subclassing or inlining code directly into the types that use it, it's a technique that's good to keep in mind when setting up the relationships between various types.
There are of course many other ways to use composition in Swift than what this post covered, in particular there are two cases that I'll write about in an upcoming post - composing functions and protocols.
What do you think? Do you use some of these composition techniques in your code base already, or is it something you'll try out? Let me know, along with any questions, comments or feedback you might have, on Twitter @johnsundell.
Thanks for reading! 🚀