DEV Community

Cover image for Design Patterns in Swift: Factory
Raphael Martin
Raphael Martin

Posted on

Design Patterns in Swift: Factory

The Factory Design Pattern

This is the first post of a series about Design Patterns! And we're starting with a very useful one for Swift applications: Factory.

This pattern consists in centralizing the object creation logic of your application in a proper place, benefiting from the abstraction concept.

If you're feeling a bit lost about abstraction, you can refer to my post about OOP in Swift.


Problem Context

When adding a new Design Pattern to your software, you should always think in which problem this pattern solves.
With Factory, we are usually trying to solve problems related to object creation, but not only it.

Coupled Lego bricks

Coupled Lego bricks

Coupling

Imagine working on a new Social Network app, and in its first version it should supports textual posts only, that will be called Articles. So, you create a custom ArtcileView class, with everything that you need for each post:

class ArticleView: UIView {
    private lazy var contentLabel: UILabel = {
        // Defines the label attributes
    }()

    private lazy var likeButton: UIButton = {
        // Defines the button attributes
    }()

    init() {
        super.init(frame: .zero)
        setupView()
    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    private func setupView() {
        backgroundColor = .white

        // Add subviews
        addSubview(contentLabel)
        addSubview(likeButton)

        // Set up constraints
        setupConstraints()
    }

    private func setupConstraints() {
        // Set the constraints
    }

    @objc private func didTapLikeButton() {
        // Make request to service, saving that the current user liked this post
    }
}

Enter fullscreen mode Exit fullscreen mode

In an implementation like that, all the logic related to the behavior of liking posts is centralized in the ArticleView. Your business logic now is tightly coupled to your view class.

In future, if you decide to implement image posts, with an ImageView for example, you'll need to duplicate the logic for the "like" action, and also add logic to determine which class to initialize:

class ContentViewManager {
    func createView(for contentType: String) -> UIView {
        if contentType == "article" {
            return ArticleView()
        } else if contentType == "image" {
            return ImageView()
        } else {
            fatalError("Unsupported content type: \(contentType)")
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

This kind of implementation has several problems, such as violation of Open/Closed Principle (OCP) and making testing harder.


What is the Factory Design Pattern?

Factory is a Design Pattern that uses factory methods to create different types of objects, that share a same more abstract type, without the need of specifying directly the concrete type of the desired object. So factories are similar to what we saw in the previous example with the ContentViewManager class, but without violating any SOLID principle and not coupling your logic.

Code Factory representation

To achieve that, Factory uses a lot the abstraction OOP concept. First, we need an abstract type to represent the objects we're trying to create. In Swift, usually protocols are used, but in other languages you can see abstract classes as well. Let's see how would it be in our Social Network app example:

protocol LikeablePost: UIView {
   func likePost()
}

class ArticleView: UIView, LikeablePost {
    // ...
}

class ImageView: UIView, LikeablePost {
    // ...
}
Enter fullscreen mode Exit fullscreen mode

Then, you also need another abstract type (we're using a protocol again) for defining how your factories should looks like:

protocol PostFactory {
    func makePost() -> LikeablePost
}
Enter fullscreen mode Exit fullscreen mode

Look that the return type of our makePost() function is our abstract type previously created. Now, we are able to create new factories every time we need to create new children of LikeablePost:

class ArtcileFactory: PostFactory {
    func makePost() -> LikeablePost { ArticleView() }
}

class ImageFactory: PostFactory {
    func makePost() -> LikeablePost { ImageView() }
}
Enter fullscreen mode Exit fullscreen mode

Now, your object creation logic is open for extension, but closed for modification, since you don't need to change any existing implementation to add support for new types, just extend it.

How to instantiate a Factory

After creating the factories, now you need some logic to decide which factory you should use when needed. For that, you could create a Resolver:

class PostFactoryResolver {
    static func getFactory(for contentType: String) -> PostFactory {
        switch contentType {
        case "article":
            return ArticleFactory()
        case "image":
            return ImageFactory()
        default:
            fatalError("Unsupported content type: \(contentType)")
        }
    }
}

// Usage:
let factory = PostFactoryResolver.getFactory(for: "article") 
let postView = factory.makePost()
Enter fullscreen mode Exit fullscreen mode

You may have noticed that this approach is very similar to what we had in our ContentViewManager, before implementing the Factory pattern. And that similarity can still violate the OCP since it will need changes when adding new types to the app. However, we have significant improvements compared to the previous implementation:

  1. Isolation: the decision logic for creating the view objects is isolated from the business logic files.
  2. Decoupling: the client code interacts only with the factories, and not with the views. Also, the views now are more testable since they are easy to mock.

A more robust approach

We can still achieve a fully OCP-compliant approach, using a combination of dynamic registration and dependency injection.
Dynamic Registration is the idea of registry object instances in a place with global visibility to be accessed by clients through an identifier. Here's an example:

class FactoryRegistry {
    private var factories: [String: PostFactory] = [:]

    func registerFactory(for contentType: String, factory: PostFactory) {
        factories[contentType] = factory
    }

    func getFactory(for contentType: String) -> PostFactory? {
        return factories[contentType]
    }
}

// Usage:
let registry = FactoryRegistry() 
// Register factories (can be done at app initialization or runtime)
registry.registerFactory(for: "article", factory: ArticleFactory())
registry.registerFactory(for: "image", factory: ImageFactory())
Enter fullscreen mode Exit fullscreen mode

Then, we can use dependency injection to inject the desired factory in the needed class.


let post = await fetchPostFromAPI(withId: "ABC1234")

// `post.type` is a String that can be "article" or "image"
if let factory = registry.getFactory(for: post.type) { 
    let postView = factory.makePost() // Use `postView` here

    // Add the postView to the view hierarchy
} else { 
    print("No factory registered for this content type.") 
}

Enter fullscreen mode Exit fullscreen mode

Now, adding a new content type doesn't require modifying any existing code, just register the new factory.

If in future, you need to support video posts, you'll just need to:

registry.registerFactory(for: "video", factory: VideoFactory())
Enter fullscreen mode Exit fullscreen mode

Disadvantages

So far, we have explored how Factory is useful for decoupling, adding flexibility and in some cases, making testing easier. However there are some cons that should be considered when thinking about implementing it in your app:

1. Complexity

Even though the examples we explored here for the post views brought several benefits, you can see that the code is now more complex. We created several different new types, including protocols and classes, increasing the size of our code base.

2. Often Not "Self-Documenting"

Without an explicit documentation for it, or yet not using a good naming convention, factories can be hard to understand at first sight. That increases the learning curve for new team members, impacting the general team's performance.

3. Violations of OCP

As we saw today, without a more robust implementation (that brings more complexity), factories can lead to a violation of the Open/Closed Principle.


Conclusion

Today we went through common problems in Software Engineering regarding objects creation and coupling, how the Factory design pattern can help with it, how to implement it and also disadvantages to consider when choosing it.
I hope that adding this new Design Pattern to your toolkit will help you in your software development journey!

Top comments (0)