Abstract Factory – Design Patterns in Swift

The abstract factory defines an interface for creating all distinct products. It leaves the actual product creation to concrete factory classes. And each factory type corresponds to a certain product variety.

And client code works with factories and products only through their abstract interfaces. This lets the client code work with any product variants, created by the factory object. You just create a new concrete factory class and pass it to the client code.

For short, the essence of the abstract factory pattern is to “Provide an interface for creating families of related or dependent objects without specifying their concrete classes.” [GoF in “Design Patterns: Abstract Factory”]

Abstract Factory – UML

The abstract factory protocol declares a set of methods that return different abstract products. These products are called a family and are related to a high-level theme or concept. Products of one family are usually able to collaborate among themselves.

import XCTest

protocol AbstractFactory {

    func createProductA() -> AbstractProductA
    func createProductB() -> AbstractProductB
}

Concrete factories produce a family of products that belong to a single variant. The factory guarantees that the resulting products are compatible. And each concrete factory has a corresponding product variant.

class ConcreteFactory1: AbstractFactory {

    func createProductA() -> AbstractProductA {
        return ConcreteProductA1()
    }

    func createProductB() -> AbstractProductB {
        return ConcreteProductB1()
    }
}

class ConcreteFactory2: AbstractFactory {

    func createProductA() -> AbstractProductA {
        return ConcreteProductA2()
    }

    func createProductB() -> AbstractProductB {
        return ConcreteProductB2()
    }
}

Each distinct product of a product family should have a base protocol. All variants of the product must implement this protocol. Concrete factories create corresponding concrete Products.

protocol AbstractProductA {

    func usefulFunctionA() -> String
}

class ConcreteProductA1: AbstractProductA {

    func usefulFunctionA() -> String {
        return "The result of the product A1."
    }
}

class ConcreteProductA2: AbstractProductA {

    func usefulFunctionA() -> String {
        return "The result of the product A2."
    }
}

Most importantly, we should make sure that all products can interact with each other. And proper interaction is possible only between products of the same concrete variant.

protocol AbstractProductB {

    func usefulFunctionB() -> String

    func anotherUsefulFunctionB(collaborator: AbstractProductA) -> String
}

class ConcreteProductB1: AbstractProductB {

    func usefulFunctionB() -> String {
        return "The result of the product B1."
    }

    func anotherUsefulFunctionB(collaborator: AbstractProductA) -> String {
        let result = collaborator.usefulFunctionA()
        return "The result of the B1 collaborating with the (\(result))"
    }
}

class ConcreteProductB2: AbstractProductB {

    func usefulFunctionB() -> String {
        return "The result of the product B2."
    }

    func anotherUsefulFunctionB(collaborator: AbstractProductA) -> String {
        let result = collaborator.usefulFunctionA()
        return "The result of the B2 collaborating with the (\(result))"
    }
}

The client code works with factories and products only through abstract types: AbstractFactory and AbstractProduct. This lets you pass any factory or product subclass to the client code without breaking it.

class Client {
    static func someClientCode(factory: AbstractFactory) {
        let productA = factory.createProductA()
        let productB = factory.createProductB()

        print(productB.usefulFunctionB())
        print(productB.anotherUsefulFunctionB(collaborator: productA))
    }
}

Let’s see how it all works together. The client code can work with any concrete factory class.

class AbstractFactoryConceptual: XCTestCase {

    func testAbstractFactoryConceptual() {

        print("Client: Testing client code with the first factory type:")
        Client.someClientCode(factory: ConcreteFactory1())

        print("Client: Testing the same client code with the second factory type:")
        Client.someClientCode(factory: ConcreteFactory2())
    }
}

Conclusion

The abstract factory pattern is pretty common in Swift code. Many frameworks and libraries use it to provide a way to extend and customize their standard components. The pattern is easy to recognize by methods, which return a factory object. And we create specific sub-components with the factory.

Leave a Reply

Your email address will not be published. Required fields are marked *