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.
Coupling
Imagine working on a new Social Network app, and in its first version it should supports textual posts only, that will be called Article
s. 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
}
}
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)")
}
}
}
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.
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 protocol
s are used, but in other languages you can see abstract class
es 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 {
// ...
}
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
}
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() }
}
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()
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:
- Isolation: the decision logic for creating the view objects is isolated from the business logic files.
- 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())
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.")
}
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())
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)