Question 3
Protocols, generics, and where clauses
How do you use protocol-oriented design with generics and associated types? When do you need a where clause on an extension or generic method?
Answer outline
Protocols, generics, and associated types help describe capabilities and relationships, not just concrete types.
Protocol-oriented design spells out what something can do. Generics keep that description reusable and type-safe across implementations.
Associated types fit when a protocol needs a relationship between types, not a single fixed concrete type.
Examples: a store tied to one model type, or a coordinator that produces one specific output type.
A where clause applies when generic code should only exist under extra constraints. It signals: this extension or method is available only when the generic types meet those conditions.
A common example of a where clause would be when checking that an associated type conforms to Equatable, Hashable, or Sendable.
Principles
- Use protocols to describe behavior and roles.
- Use generics to write reusable, type-safe code.
- Use associated types when a protocol depends on a related type.
- Keep abstractions meaningful, not overly abstract.
- Prefer compile-time guarantees over loose runtime checks.
- Use constrained extensions to add specialized behavior cleanly.
Every Cache works with some Value, but different caches can choose different value types.
protocol Cache {
associatedtype Value
func save(_ value: Value, for key: String)
func load(for key: String) -> Value?
}
A single implementation can be reused for any value type.
final class MemoryCache<T>: Cache {
private var storage: [String: T] = [:]
func save(_ value: T, for key: String) {
storage[key] = value
}
func load(for key: String) -> T? {
storage[key]
}
}
This extension only makes sense when Value is Equatable, because == is used—that is when a where clause is needed.
extension Cache where Value: Equatable {
func contains(_ value: Value, for key: String) -> Bool {
load(for: key) == value
}
}
The where clause adds the rule that T must conform to Equatable (equivalent to writing <T: Equatable> on the first form).
func areEqual<T: Equatable>(_ lhs: T, _ rhs: T) -> Bool {
lhs == rhs
}
func compare<T>(_ lhs: T, _ rhs: T) -> Bool where T: Equatable {
lhs == rhs
}
This extension exists only when the repository’s Model is Sendable.
protocol Repository {
associatedtype Model
func save(_ model: Model)
}
extension Repository where Model: Sendable {
func saveSafely(_ model: Model) {
save(model)
}
}