No history yet

Protocol-Oriented Creational Patterns

Scaling Object Creation

You already know how to create objects using initializers. But as applications grow, direct initialization (let myObject = MyClass()) can lead to rigid and tightly coupled code. When you need more flexibility, creational patterns provide powerful alternatives. Let's explore how to implement these patterns using Swift's strengths, particularly protocols and generics.

The Factory Method pattern replaces direct object creation calls with a call to a special method, the “factory.” This decouples your code from concrete classes. Instead of knowing exactly which type to create, your code just asks the factory for a new object that conforms to a specific protocol. We can make this even more powerful with Generics to create a single factory capable of producing different types of objects.

// 1. Define the protocol for our products
protocol Document {
    var name: String { get set }
    func open()
}

// 2. Create concrete products
struct TextDocument: Document {
    var name: String
    func open() {
        print("Opening text document: \(name)")
    }
}

struct SpreadsheetDocument: Document {
    var name: String
    func open() {
        print("Opening spreadsheet: \(name)")
    }
}

// 3. Implement the generic factory
class DocumentFactory {
    func createDocument<T: Document>(_ type: T.Type, name: String) -> T {
        return T(name: name)
    }
}

// Usage
let factory = DocumentFactory()
let report = factory.createDocument(TextDocument.self, name: "Q3_Report.txt")
report.open() // Prints: Opening text document: Q3_Report.txt

let budget = factory.createDocument(SpreadsheetDocument.self, name: "2024_Budget.xlsx")
budget.open() // Prints: Opening spreadsheet: 2024_Budget.xlsx

By using a generic function createDocument<T: Document>, the factory isn't tied to any single document type. It can create any object that conforms to the Document protocol and has a matching initializer, making our system much more extensible.

Factories for Families

What if you need to create a whole family of related objects? For example, a UI theme might require a specific style for buttons, labels, and text fields. The Abstract Factory pattern handles this. It provides an interface for creating families of related objects without specifying their concrete classes.

This is a perfect use case for Protocol-Oriented Programming in Swift. We can define a protocol for our factory, and then create concrete factories for each theme (like LightThemeFactory and DarkThemeFactory).

// 1. Define protocols for the family of products
protocol Button {
    func press()
}

protocol Label {
    func renderText()
}

// 2. Define the Abstract Factory protocol
protocol UIFactory {
    func createButton() -> Button
    func createLabel() -> Label
}

// 3. Create concrete products for the "Light" theme
struct LightButton: Button {
    func press() { print("Light button pressed.") }
}
struct LightLabel: Label {
    func renderText() { print("Rendering text in light theme.") }
}

// 4. Create a concrete factory for the "Light" theme
struct LightThemeFactory: UIFactory {
    func createButton() -> Button {
        return LightButton()
    }
    func createLabel() -> Label {
        return LightLabel()
    }
}

// Imagine we also have DarkButton, DarkLabel, and DarkThemeFactory...

// Usage
let factory: UIFactory = LightThemeFactory() // Can be swapped with DarkThemeFactory

let loginButton = factory.createButton()
let greetingLabel = factory.createLabel()

loginButton.press()

The client code only knows about the UIFactory, Button, and Label protocols. It has no idea whether it's dealing with a LightThemeFactory or a DarkThemeFactory. This allows you to switch out the entire UI theme by just instantiating a different factory, without changing any of the client code that uses it.

Constructing Complex Objects

Sometimes, creating an object is a multi-step process. Think of a URLRequest in networking: you need to set the URL, the HTTP method, headers, and maybe a body. Using a massive initializer with a dozen parameters is clumsy and error-prone.

The Builder pattern solves this by separating the construction of a complex object from its representation. You use a Builder object to configure the final product step-by-step and then call a build() method to get the fully configured object.

struct NetworkRequest {
    let url: URL
    let method: String
    let headers: [String: String]
    let body: Data?
}

class NetworkRequestBuilder {
    private var url: URL
    private var method: String = "GET"
    private var headers: [String: String] = [:]
    private var body: Data? = nil

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

    func setMethod(_ method: String) -> Self {
        self.method = method
        return self
    }

    func addHeader(key: String, value: String) -> Self {
        self.headers[key] = value
        return self
    }

    func setBody(_ body: Data) -> Self {
        self.body = body
        return self
    }

    func build() -> NetworkRequest {
        return NetworkRequest(url: url, method: method, headers: headers, body: body)
    }
}

// Usage
let request = NetworkRequestBuilder(url: URL(string: "https://api.example.com/users")!)
    .setMethod("POST")
    .addHeader(key: "Content-Type", value: "application/json")
    .setBody("{\"name\": \"Alex\"}".data(using: .utf8)!)
    .build()

print(request.method) // POST
print(request.headers) // ["Content-Type": "application/json"]

This fluent interface, where each method returns Self, allows for chaining calls together in a clean, readable way. The builder ensures that a NetworkRequest is only created once all configuration is complete and in a valid state.

Cloning, Swift-Style

The Prototype pattern is about creating new objects by copying an existing instance, known as the prototype. This is useful when the cost of creating a new object from scratch is higher than cloning an existing one.

In Swift, the implementation of this pattern depends heavily on whether you're using a value type (struct) or a reference type (class). Swift's powerful Value Semantics mean that structs already behave like prototypes out of the box.

When you assign a struct to a new variable, you get a full, independent copy. No extra work needed.

Classes, however, are reference types. Assigning a class instance to a new variable just creates another reference to the same object in memory. To achieve true cloning with classes, you need to implement the functionality yourself, often via a clone() method.

// 1. For Structs, cloning is implicit.
struct GameConfiguration {
    var difficulty: Int
    var enableSound: Bool
}

let easyConfig = GameConfiguration(difficulty: 1, enableSound: true)
var hardConfig = easyConfig // A new copy is created automatically.
hardConfig.difficulty = 10

print(easyConfig.difficulty) // 1
print(hardConfig.difficulty) // 10

// 2. For Classes, you must implement cloning explicitly.
protocol Clonable {
    func clone() -> Self
}

class UserProfile: Clonable {
    var username: String
    var followerCount: Int

    init(username: String, followerCount: Int) {
        self.username = username
        self.followerCount = followerCount
    }

    func clone() -> Self {
        // Required to conform to the protocol and instantiate a new object
        return UserProfile(username: self.username, followerCount: self.followerCount) as! Self
    }
}

let originalUser = UserProfile(username: "beta_tester", followerCount: 100)
let clonedUser = originalUser.clone()
clonedUser.username = "release_user"

print(originalUser.username) // beta_tester
print(clonedUser.username)   // release_user

The Clonable protocol establishes a clear contract for any class that should be copyable. When deciding between a class and a struct, consider whether you need a shared reference or unique, copied instances. If it's the latter, a struct might be the simpler choice.

Choosing the Right Pattern

Each creational pattern solves a different problem. Choosing the right one depends on the complexity of the object and the level of abstraction you need.

PatternWhen to Use It
Factory MethodWhen you want to delegate the instantiation of a single object type to a subclass or separate method. Great for decoupling a client from a concrete product.
Abstract FactoryWhen you need to create families of related objects (e.g., a complete UI theme) without specifying concrete classes.
BuilderWhen creating an object is a complex, multi-step process with many configuration options. It improves readability over a complex initializer.
PrototypeWhen creating an object is expensive and it's cheaper to copy an existing one. In Swift, prefer structs for this unless you specifically need class features.

Understanding these patterns and how to implement them with a protocol-oriented approach will make your Swift code more flexible, maintainable, and testable. They provide a robust toolkit for managing object creation as your applications scale.

Quiz Questions 1/5

What is the primary benefit of using the Factory Method pattern instead of directly initializing an object with MyClass()?

Quiz Questions 2/5

Which creational pattern is most appropriate for creating families of related objects, such as a set of UI components for a 'Light Theme' and a corresponding set for a 'Dark Theme'?