
Large applications often need to support multiple behaviors that follow the same interface but differ in implementation. This appears in many areas such as:
- error handling
- analytics tracking
- network policies
- design systems and reusable UI components
A common but problematic approach is to use enums combined with switch statements. While simple at first, this quickly leads to rigid code that is difficult to extend.
A better approach is the Strategy Pattern, which allows behavior to be encapsulated and swapped without modifying existing code.
The Problem with Enum-Based Behavior
A typical implementation might look like this:
enum ErrorType {
case generic
case apiError
case connectionError
}
Behavior is then resolved using switch:
struct ServerError {
let errorType: ErrorType
var title: "String {"
switch errorType {
case .generic: return "Generic error"
case .apiError: return "Api error"
case .connectionError: return "Connection error"
}
}
var subtitle: "String {"
switch errorType {
case .generic: return "An error has occurred"
case .apiError: return "Api failed to response"
case .connectionError: return "Lost connection"
}
}
}
This approach introduces several issues:
- new cases require modifying existing code
- logic becomes centralized in large switch blocks
- it violates the Open/Closed Principle
- shared enums can create tight coupling between modules
Instead of selecting behavior with conditionals, we can encapsulate each behavior into its own object.
Introducing the Strategy Pattern
The Strategy Pattern defines a family of behaviors, encapsulates each one, and makes them interchangeable.
In Swift, this fits naturally with protocol-oriented programming.
protocol ServerErrorProtocol {
var title: String { get }
var subtitle: String { get }
}
Each concrete implementation defines its own behavior.
struct GenericServerError: ServerErrorProtocol {
let title = "Generic error"
let subtitle = "An error has occurred"
}
struct ApiServerError: ServerErrorProtocol {
let title = "Api error"
let subtitle = "Api failed to response"
}
struct ConnectionServerError: ServerErrorProtocol {
let title = "Connection error"
let subtitle = "Lost connection"
}
Usage becomes straightforward:
func displayError(_ error: ServerErrorProtocol) {
print(error.title)
print(error.subtitle)
}
displayError(GenericServerError())
No switch statements are required.
Benefits
Open for Extension
Adding a new behavior only requires a new strategy:
struct TimeoutServerError: ServerErrorProtocol {
let title = "Timeout"
let subtitle = "The request timed out"
}
Better Separation of Concerns
Consumers only depend on the protocol, not on concrete implementations.
Easier Testing
Strategies can easily be mocked.
struct MockServerError: ServerErrorProtocol {
let title: String
let subtitle: String
}
Example test:
func testDisplayErrorUsesProvidedStrategyValues() {
let error = MockServerError(
title: "Mock title",
subtitle: "Mock subtitle"
)
XCTAssertEqual(error.title, "Mock title")
XCTAssertEqual(error.subtitle, "Mock subtitle")
}
Tests become:
- more isolated
- easier to read
- less coupled to implementation details
Strategy in Modular Architectures
This pattern becomes particularly useful in modular architectures.
Using shared enums forces modules to modify shared code when adding new cases. This increases coupling and can trigger unnecessary module rebuilds.
With the Strategy Pattern, each module can define its own behavior without modifying shared code.
struct PaymentDeclinedError: ServerErrorProtocol {
let title = "Payment declined"
let subtitle = "Your payment method was rejected"
}
Strategy in Design Systems
The same approach can also be applied to Design Systems.
Instead of using enums to configure components (such as styles or variants), strategies allow each configuration to define its own behavior.
For example, a button component could receive a strategy describing how it should render:
protocol ButtonStyleStrategy {
var backgroundColor: UIColor { get }
var textColor: UIColor { get }
}
Different styles can then be implemented independently.
This keeps the component flexible without requiring large enums or switch statements inside the UI layer.
Using Singleton Strategies (Optional)
When strategies are stateless and immutable, creating new instances repeatedly may be unnecessary.
In these cases, strategies can be exposed as shared instances.
final class GenericServerError: ServerErrorProtocol {
static let shared = GenericServerError()
let title = "Generic error"
let subtitle = "An error has occurred"
private init() {}
}
Usage:
displayError(GenericServerError.shared)
Benefits include:
- avoiding repeated allocations
- guaranteeing a single instance across the app
- making strategies behave like reusable configuration objects
This pattern is particularly useful in design systems and shared UI configurations.
Final Thoughts
The Strategy Pattern is a simple but powerful tool for improving flexibility and modularity.
By encapsulating behavior into interchangeable objects, we can:
- remove conditional logic
- follow SOLID principles
- reduce coupling between modules
- enable scalable architectures
Combined with Swift’s protocol-oriented design, Strategy becomes a natural and elegant solution for building maintainable systems.
Top comments (0)