Swift Design Patterns: Exploring Factories with Protocols and Closures
In the world of Swift programming, design patterns play a crucial role in creating scalable and maintainable code. Among these patterns, the factory pattern stands out as a powerful solution for object creation, enabling developers to encapsulate the instantiation logic away from the client code.
In this post, we’ll explore two distinct approaches to implementing factories in Swift: one utilizing protocols and the other leveraging closures.
By examining these methodologies, we’ll uncover their unique benefits, trade-offs, and practical applications. Whether you’re building a small app or a complex system, understanding these patterns will help you write cleaner, more flexible code. Let's dive into the nuances of factory design patterns in Swift and discover which approach might best suit your development needs!

Using Protocols for Factory Design
Let's take a look at this code:
protocol CarProtocol {
func start()
}
protocol CarFactoryProtocol {
func makeCar() -> CarProtocol
}
class CarProvider {
private let carFactory: CarFactoryProtocol
init(carFactory: CarFactoryProtocol) {
self.carFactory = carFactory
}
func provide() -> CarProtocol {
carFactory.makeCar()
}
}
class Miata: CarProtocol {
func start() {
print("Starting Miata...")
}
}
class MazdaFactory: CarFactoryProtocol {
func makeCar() -> any CarProtocol {
Miata()
}
}
let aCarProvider = CarProvider(carFactory: MazdaFactory())
let mazda = aCarProvider.provide()
mazda.start()
class Toyota86: CarProtocol {
func start() {
print("Starting 86...")
}
}
class ToyotaFactory: CarFactoryProtocol {
func makeCar() -> any CarProtocol {
return Toyota86()
}
}
let bCarProvider = CarProvider(carFactory: ToyotaFactory())
let toyota = bCarProvider.provide()
toyota.start()This approach emphasizes type safety and encapsulation, ensuring that the client code remains decoupled from the specific implementations of the car types.
We begin by defining two protocols: CarProtocol and CarFactoryProtocol. The CarProtocol outlines the essential behavior for any car by requiring a start() method. The CarFactoryProtocol serves as a blueprint for factories, mandating the implementation of the makeCar() method that returns a CarProtocol.
Next, we implement concrete car types, starting with the Miata class. This class conforms to CarProtocol and provides a specific implementation for the start() method, demonstrating how each car can exhibit its own unique behavior. Similarly, we create a Toyota86 class that also adheres to the CarProtocol.
The CarProvider class acts as an intermediary, taking a factory that conforms to CarFactoryProtocol. By injecting a factory into the provider, we ensure that the car creation logic is separated from the provider itself. This makes the system flexible, allowing us to introduce new car types and factories without altering existing code.
For instance, the MazdaFactory class implements CarFactoryProtocol, and its makeCar() method produces a Miata instance. Similarly, the ToyotaFactory does the same for Toyota86.
When we create instances of CarProvider with specific factories and call the provide() method, we receive a car instance ready to start. This structure allows for easy extension and modification: adding new car types merely requires implementing a new class and factory, showcasing the power of protocols in maintaining a clean and manageable codebase.
This protocol-oriented design not only enhances readability but also adheres to key principles of object-oriented design, such as the Open/Closed Principle, making our application more robust and adaptable to future requirements.
Simplifying Factories with Closures
Let's look at an example:
typealias CarFactory = () -> CarProtocol
class ZCarProvider {
private let carFactory: CarFactory
init(carFactory: @escaping CarFactory) {
self.carFactory = carFactory
}
func provide() -> CarProtocol {
carFactory()
}
}
class Civic: CarProtocol {
func start() {
print("Starting Civic...")
}
}
let cCarProvider = ZCarProvider(carFactory: Civic.init)
let honda = cCarProvider.provide()
honda.start()By replacing the traditional factory protocols with a closure-based mechanism, we can simplify object creation and reduce boilerplate code.
We start by defining a CarFactory typealias, which represents a closure that returns an instance conforming to the CarProtocol. This abstraction allows for greater flexibility and clarity in how car objects are instantiated.
The ZCarProvider class takes advantage of this closure-based factory. Instead of requiring a concrete factory class, it accepts a CarFactory closure during initialization. This means we can easily pass in a method reference for creating a car, such as Civic.init, without the need for a dedicated factory class.
In this implementation, we define a Civic class that conforms to CarProtocol, providing a specific implementation of the start() method. This simple structure allows us to create instances of ZCarProvider with ease. By passing Civic.init as the factory closure, we set up the provider to generate Civic instances on demand.
When we call the provide() method on the ZCarProvider instance, the closure is invoked, resulting in the creation of a Civic object. This approach not only minimizes the overhead associated with factory classes but also enhances code readability by directly associating the factory logic with the car type being created.
The closure-based design offers a flexible alternative to traditional factory patterns, particularly useful in scenarios where simplicity and rapid development are prioritized. This method allows developers to quickly adapt to changing requirements, making it an excellent choice for projects that may evolve over time.
In summary, using closures for factory design in Swift can lead to cleaner, more maintainable code while still adhering to the principles of encapsulation and separation of concerns. This approach emphasizes efficiency without sacrificing the clarity and structure that Swift developers value.
Key Differences
Flexibility and Simplicity:
The second snippet offers greater flexibility by using closures. It eliminates the need for separate factory classes, making it easier to create car instances without the overhead of a factory object.
Type Safety:
The first snippet maintains strong type safety by explicitly using protocols for factories, which may be advantageous in larger systems where clear interfaces are beneficial.
Extensibility:
The first approach can be extended easily with new car types and factories, adhering to the Open/Closed Principle. The second snippet is also extensible, but adding new car types may require additional closures or factory functions.
Readability:
The second snippet might be considered more readable and concise due to its use of closures, while the first snippet provides a more traditional object-oriented approach.
Initialization:
The first snippet requires the creation of a factory instance, while the second can directly reference an initializer, making it less verbose.
Conclusion
Choosing the right design pattern for your project can significantly influence code quality and maintainability. Opt for the protocol-based approach when you need a structured, type-safe design with clear interfaces—especially beneficial for larger projects. Alternatively, if you value simplicity and flexibility with less boilerplate code, the closure-based approach can help you achieve rapid development, making it ideal for smaller projects or quick prototypes.
Ultimately, both designs are valid, and understanding their nuances allows you to make informed decisions that align with your project’s complexity and requirements. Experimenting with both approaches will enhance your skills as a Swift developer and lead to better design practices in your applications.




Comments