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
Requestwrapping 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
Observabletype that wraps the source and adds behavior. - React and Flutter: These take composition to the next level—small, reusable components compose into sophisticated UIs.