Journey before destination, tests before production

The Power of Composition

September 01, 2018

space station

Object composition is fundamental to OOP—and for good reason. Real-world systems are built from smaller parts, and software models this naturally through composition.

This article won’t argue why composition beats inheritance. Instead, I want to show an example where composition really shines.

Building requests through composition

Consider an interface for producing URLRequest objects:

protocol Request {
    func request() -> URLRequest
}

A basic implementation:

struct RequestWithURL: Request {
    private let url: URL

    init(url: URL) {
        self.url = url
    }

    func request() -> URLRequest {
        URLRequest(url: url)
    }
}

Now, a POST request:

struct POSTRequest: Request {
    private let base: Request

    init(base: Request) {
        self.base = base
    }

    func request() -> URLRequest {
        var request = base.request()
        request.httpMethod = "POST"
        return request
    }
}

Notice the pattern: POSTRequest wraps another Request and adds behavior. It doesn’t care what kind of request it wraps.

A Bearer authentication header:

struct BearerRequest: Request {
    private let base: Request
    private let token: String

    init(token: String, base: Request) {
        self.base = base
        self.token = token
    }

    func request() -> URLRequest {
        var request = base.request()
        request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
        return request
    }
}

The power here: small, focused types that compose into complex behavior.

Composing requests

Let’s build an authenticated POST request with a body. First, a body wrapper:

struct RequestWithBody: Request {
    private let base: Request
    private let data: Data

    init(data: Data, base: Request) {
        self.data = data
        self.base = base
    }

    func request() -> URLRequest {
        var request = base.request()
        request.httpBody = data
        return request
    }
}

Now compose them:

let request = BearerRequest(
    token: "SuperSecretToken",
    base: RequestWithBody(
        data: Data("Hello World".utf8),
        base: POSTRequest(
            base: RequestWithURL(
                url: URL(string: "http://example.com")!
            )
        )
    )
)

Verbose? Perhaps. But also declarative—each type name describes exactly what it does. You can read the composition and understand the request being built.

The real advantage is maintainability. Each Request type:

  • Does one thing
  • Is easy to test in isolation
  • Can be reused in any combination

Fluent interface with view modifier style

The nested initializers are readable but awkward. SwiftUI popularized a better pattern: view modifiers. We can apply the same idea here.

Add methods to the Request protocol that return wrapped requests:

extension Request {
    func post() -> some Request {
        POSTRequest(base: self)
    }

    func body(_ data: Data) -> some Request {
        RequestWithBody(data: data, base: self)
    }

    func bearer(token: String) -> some Request {
        BearerRequest(token: token, base: self)
    }
}

Now the same composition reads naturally:

let request = RequestWithURL(url: URL(string: "http://example.com")!)
    .post()
    .body(Data("Hello World".utf8))
    .bearer(token: "SuperSecretToken")

This is the builder pattern meets composition. Each modifier:

  • Returns a new Request wrapping the previous one
  • Preserves immutability
  • Chains naturally left-to-right

The underlying types haven’t changed—we just added ergonomic sugar on top. The composition is identical, but the call site is dramatically cleaner.

Libraries built on composition

  • SwiftUI: The entire framework is composition. Views wrap other views, modifiers wrap views and return new views. A Text("Hello").padding().background(Color.red) chain creates a tree of composed view types—each modifier is a struct wrapping the previous one.
  • RxSwift: The public API is FRP, but internally each operator is a new Observable type that wraps the source and adds behavior.
  • React and Flutter: These take composition to the next level—small, reusable components compose into sophisticated UIs.

Serg Dort

Written by Serg Dort. Say one thing for him: he ships code.