
Posted on • Edited 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 theabstraction concept.
If you're feeling a bit lost about abstraction, you can refer tomy post about OOP in Swift.
Problem Context
When adding a newDesign Pattern to your software, you should always think in which problem this pattern solves.
WithFactory, 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 calledArticle
s. So, you create a customArtcileView
class, with everything that you need for each post:
classArticleView:UIView{privatelazyvarcontentLabel:UILabel={// Defines the label attributes}()privatelazyvarlikeButton:UIButton={// Defines the button attributes}()init(){super.init(frame:.zero)setupView()}requiredinit?(coder:NSCoder){fatalError("init(coder:) has not been implemented")}privatefuncsetupView(){backgroundColor=.white// Add subviewsaddSubview(contentLabel)addSubview(likeButton)// Set up constraintssetupConstraints()}privatefuncsetupConstraints(){// Set the constraints}@objcprivatefuncdidTapLikeButton(){// Make request to service, saving that the current user liked this post}}
In an implementation like that, all the logic related to the behavior ofliking posts
is centralized in theArticleView
. Your business logic now istightly coupled to your view class.
In future, if you decide to implement image posts, with anImageView
for example, you'll need to duplicate the logic for the "like" action, and also add logic to determine which class to initialize:
classContentViewManager{funccreateView(forcontentType:String)->UIView{ifcontentType=="article"{returnArticleView()}elseifcontentType=="image"{returnImageView()}else{fatalError("Unsupported content type:\(contentType)")}}}
This kind of implementation has several problems, such asviolation of Open/Closed Principle (OCP) andmaking testing harder.
What is the Factory Design Pattern?
Factory is a Design Pattern that usesfactory 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 theContentViewManager
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, usuallyprotocol
s are used, but in other languages you can seeabstract class
es as well. Let's see how would it be in our Social Network app example:
protocolLikeablePost:UIView{funclikePost()}classArticleView:UIView,LikeablePost{// ...}classImageView:UIView,LikeablePost{// ...}
Then, you also need another abstract type (we're using aprotocol
again) for defining how your factories should looks like:
protocolPostFactory{funcmakePost()->LikeablePost}
Look that the return type of ourmakePost()
function is our abstract type previously created. Now, we are able to createnew factories every time we need to create new children ofLikeablePost
:
classArtcileFactory:PostFactory{funcmakePost()->LikeablePost{ArticleView()}}classImageFactory:PostFactory{funcmakePost()->LikeablePost{ImageView()}}
Now, your object creation logic isopen for extension, butclosed 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 aResolver:
classPostFactoryResolver{staticfuncgetFactory(forcontentType:String)->PostFactory{switchcontentType{case"article":returnArticleFactory()case"image":returnImageFactory()default:fatalError("Unsupported content type:\(contentType)")}}}// Usage:letfactory=PostFactoryResolver.getFactory(for:"article")letpostView=factory.makePost()
You may have noticed that this approach is very similar to what we had in ourContentViewManager
, before implementing theFactory 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 ofdynamic registration anddependency injection.
Dynamic Registration is the idea ofregistry object instances in a place with global visibility to be accessed by clients through an identifier. Here's an example:
classFactoryRegistry{privatevarfactories:[String:PostFactory]=[:]funcregisterFactory(forcontentType:String,factory:PostFactory){factories[contentType]=factory}funcgetFactory(forcontentType:String)->PostFactory?{returnfactories[contentType]}}// Usage:letregistry=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 usedependency injection to inject the desired factory in the needed class.
letpost=awaitfetchPostFromAPI(withId:"ABC1234")// `post.type` is a String that can be "article" or "image"ifletfactory=registry.getFactory(for:post.type){letpostView=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 howFactory 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 morecomplex. 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 theFactory 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)
For further actions, you may consider blocking this person and/orreporting abuse