Portuguese version: https://dev.to/ianito/design-patterns-factory-method-pt-br-21a3
Hi there,
If you don't know me, my name is Iaan Mesquita.
I'm a software engineer, and today I'll start a series of articles that talk a little bit about the Software Engineering/Software Architecture field.
But why are design patterns so important? Well, they provide solutions for common problems that we, as software engineers, need to deal. Using design patterns can make software easier to understand (though not always), extend, and maintain.
I recommend starting with this article, as it introduces concepts that will be foundational for the upcoming ones. To avoid redundancy, I advise familiarizing yourself with these basics.
Note: I won't always follow best practices when explaining a specific design pattern (since my goal is to focus on the concept at hand), please consider if your suggestions would simplify things for those unfamiliar with patterns. All feedback is welcome.
And also, it's good to have an understanding of Object-Oriented Programming (OOP) and SOLID principles.
Summary
- Readability vs Writability
- Dependecy Injection
- Creational, Structural, and Behavioral Patterns
- Problem
- Factory Method
- Pros & Cons
- Conclusion
- References
Before start discussing Design Patterns, let's take a look at some concepts:
Readability vs Writability
Readability
It means the ease with which a code can be read and understood by other developers.
Writability
It means the ease with which a code can be written. This is nothing about the velocity of typing, we can understand this concept by the easiness of expressing our ideas as a software engineer. If you ever worked with C language, you can understand this concept by remembering string manipulation there and compare with another language.
These two concepts are so important in the Software Engineering field (not only). In general, you won't create new things but enhance things that already exist and for that, readability and writability can affect our software in the long term.
If you could choose between software that you can read/write with facility or software with every developer decided to follow your own patterns, what do you will chose?
Unless you are trying to challenge yourself, I know the answer. xD
However, this doesn't imply that every common pattern known offers optimal writability or readability. At times, we might have to prioritize one over the other for reasons like enhanced extensibility or specific project/team constraints.
Otherwise understanding these concepts is crucial when deciding when and how to apply them.
Dependency Injection
Dependency Injection (DI) is a software design pattern that deals with how components or objects acquire their dependencies. Instead of an object creating its own dependencies (like instantiating your dependencies) or using global instances, dependencies are "injected" into the object, typically through its constructor, a method, or properties. This allows for greater flexibility, testability, and maintainability in software design.
We can discuss this pattern in the future.
Creational, Structural, and Behavioral Patterns
Creational Patterns focus on optimal ways to instantiate objects. These patterns offer various techniques, besides using constructors, to ensure flexibility and maintability.
Structural Patterns guide the composition of objects or classes, aiming for scalability and maintainability.
Behavioral Patterns address how objects interact and communicate, emphasizing objects' responsibilities and their collaboration.
Today we will discuss Factory Method - (Creational Pattern)
Problem
Imagine that you’re creating an application to help your customer to manage their Instagram accounts. As well as you are a startup, you are validating your ideas and the first and only feature that your app has is the ability to post on Instagram.
Now your customers loved your application and they are missing other social networks like X, Facebook, BlueSky, and whatever and you decided to implement them. I don't know yet which social api I'm going to use, but I know it's an API.
However, you know that every social network has its own method way to post into them, with different dependencies and validations for example, but you know the behavior is the same: Post inside a social network.
Are you agree with me that every place inside your application that you need to post inside a social network, doesn't need to know about implementation? Just work as expected and post inside a social network?
For this instance, let's assume that you are designing your application using Dependency Injection passing dependencies through the constructor, you would need to inject them into the constructor, like:
new TwitterAPI(messageValidator,twitterSDK);
//or and
new FacebookAPI(messageValidator,userIdHash,facebookSDK);
//or and
new InstagramAPI(instagramSDK);
Don't think so much about details, just catch the main idea, I'm passing some dependencies that my class needs.
Let's imagine too that I'll need to use FacebookAPI/TwitterAPI in a lot of places, and every time I wanted to use methods from class we first need to instantiate it and pass its dependencies through the constructor.
But not only this, what happens if Facebook decided that they won't let you post inside a platform before you confirm that you're not a robot? As a result, you will need to validate this before sending, and now you have a lot of places to make this change because one thing changed in the way that Facebook APIs communicate.
So, one solution to this problem is using Factories.
Factory Method
The Factory Method is a creational design pattern that provides an interface for creating objects but allows subclasses to change the type of an object that will be created and objects returned by a factory method are often referred to as products.
The main idea of the Factory Method is to delegate the implementation of the creation code to its subclasses or classes that implement it.
But there is a limitation though: subclasses may return different types of products only if these products have a common base class or interface(APIIntegration). Also, the factory method in the base class should have its return type(APIIntegration) declared as this interface.
Let's understand some concepts about Factory Method using our example below:
Product: This is an interface or abstract class defining the type of objects the factory method will create.
In our example: Interface APIIntegration
Creator: This class declares the factory method itself. This method returns an object of type Product. In some implementations, the Creator might be an interface, an abstract class, or even a concrete class.
In our example: Interface APIFactory with createApi methods and return a product of type APIImplementation
ConcreteCreator: A class that implements or extends the Creator and overrides the factory method to return an instance of a specific ConcreteProduct.
In our example: TwitterAPIFactory, FacebookAPIFactory, and InstagramAPIFactory
ConcreteProduct: This is a specific implementation of the Product. This is what the ConcreteCreator will instantiate and return.
In our example: TwitterAPI, FacebookAPI, InstagramAPI
So, let's see the code:
Don't take the details of how I connected to APIS too seriously or syntax, all the examples are fake and not based on the main documentation. Just absorb the idea.
The dependencies of the ConcreteProduct's classes are passed through the constructor. (DI)
Product
interface APIIntegration {
post(message: string): void;
}
Creator
export interface APIFactory {
createAPI(): APIIntegration;
}
ConcreteProduct
class TwitterAPI implements APIIntegration {
// Private and readonly is the same as initializing the property through constructor method
// this.messageValidator = messageValidator
// and so on
constructor(private readonly messageValidator: MessageValidator, private readonly twitterSdk: TwitterSDK) {}
post(message: string) {
// A code to make it possible(Implementation)
// Fake code below
if (!this.messageValidator.validate(message))
throw new Error("invalid validation");
if (!this.twitterSdk.isConnected) throw new Error("connection refused");
this.twitterSdk.post(message);
console.log(`Posting on twitter: ${message}`);
}
}
class FacebookAPI implements APIIntegration {
// Private and readonly is the same as initializing the property through constructor method
// this.messageValidator = messageValidator
// and so on
constructor(
private readonly messageValidator: MessageValidator,
private readonly userIdHash: string,
private readonly facebookSDK: FacebookSDK
) {} //New param (userIdHash)
post(message: string) {
// A code to make it possible(Implementation)
// Fake code below
if (!this.messageValidator.validate(message))
throw new Error("invalid validation");
if (!this.facebookSDK.isConnected) throw new Error("connection refused");
if (!this.facebookSDK.findUser(userIdHash))
throw new Error("user not found");
this.facebookSDK.post(message);
console.log(`Posting on facebook: ${message}`);
}
}
class InstagramAPI implements APIIntegration {
// Private and readonly is the same as initializing the property through constructor method
constructor(private readonly instagramSDK: InstagramSDK) {} //New params (userIdHash)
post(message: string) {
// A code to make it possible(Implementation)
// Fake code below
this.instagramSDK.post(message);
console.log(`Posting on instagram: ${message}`);
}
}
ConcreteCreator
class TwitterAPIFactory implements APIFactory {
createAPI(): APIIntegration {
const messageValidator = new MessageValidator()
const twitterSDK = new TwitterSDK()
return new TwitterAPI(messageValidator,twitterSDK);
}
}
class FacebookAPIFactory implements APIFactory {
createAPI(): APIIntegration {
const messageValidator = new MessageValidator()
const facebookSDK = new FacebookSDK()
const userIdHash = "123"
return new FacebookAPI(messageValidator,userIdHash,facebookSDK);
}
}
class InstagramAPIFactory implements APIFactory {
createAPI(): APIIntegration {
const instagramSDK = new InstagramSDK()
return new InstagramAPI(instagramSDK);
}
}
Client code
const allSocialNetworks:APIFactory[] = [new InstagramAPIFactory(),new FacebookAPIFactory(),new TwitterAPIFactory()];
for (const api of allSocialNetworks) {
const socialNetworkAPI = api.createAPI()
socialNetworkAPI.post('Hello Followers')
}
With this design pattern, if I need to modify the dependencies or even update the packages that Facebook, Twitter, or Instagram use, I just need to adjust my ConcreteCreator and/or ConcreteProduct as long as I respect my contract APIIntegration(Product) and APIFactory(Creator).
Anywhere in my code where I utilize my API remains unchanged and unaffected by these shifts. This isolates potential sources of errors and minimizes the areas of code we must update, decreasing the chances of introducing bugs.
If I ever decide to integrate another social network, like Threads, it's as simple as crafting its specific ConcreteProduct and ConcreteFactory. Then, I can call it within my client code, without disrupting the existing setup.
class ThreadsAPI implements APIIntegration {
constructor(private readonly threadsSDK: ThreadsSDK) {}
post(message: string) {
// A code to make it possible(Implementation)
// Fake code below
this.threadsSDK.post(message);
console.log(`Posting on Threads: ${message}`);
}
}
class ThreadsAPIFactory implements APIFactory {
createAPI(): APIIntegration {
const threadsSDK = new ThreadsSDK();
return new ThreadsAPI(threadsSDK);
}
}
And if I also need to create tests to test this behavior, I don't need to use a real implementation. I can create a specific ConcreteProduct and ConcreteFactory that simulate this behavior.
Some points about Factory Method
When we talk about software design, it's not just about making the code work. It's also about ensuring that the code can evolve and we need to have in mind that everything changes. Today's technology might be obsolete tomorrow.
Therefore, our code needs to be flexible enough to accommodate these changes without having to create everything again.
The Factory Method Pattern is good in these scenarios:
Abstraction: It provides a buffer between the client code and the object instantiation process. This abstraction ensures that even if the creation logic becomes complex in the future, it won't ripple and affect other parts of the application.
Extensibility: Want to add another social media platform to your application? With the Factory Method, you can effortlessly introduce a new ConcreteProduct (like a new API integration) without changing much of your existing code.
Maintainability: As mentioned earlier, changes are localized. If the API of a particular platform changes, we only have to modify its respective factory and product, leaving the rest of the application untouched.
This pattern, while not a silver bullet for all problems, offers a good approach to object creation. It encapsulates the complexities and provides a clear structure, making the code more understandable and less prone to errors.
Pros and Cons
Like any tool or approach, the Factory Method Pattern has its advantages and disadvantages:
Pros
You avoid tight coupling between the creator and the concrete products.
Single Responsibility Principle. You can move the product creation code into one place in the program, making the code easier to support.
Open/Closed Principle. You can introduce new types of products into the program without breaking existing client code.
Cons
The code may become more complicated since you need to introduce a lot of new subclasses to implement the pattern. The best-case scenario is when you’re introducing the pattern into an existing hierarchy of creator classes.
If your class doesn't need a lot of dependencies this pattern can add over-engineering.
If the application's dependency requirements are not expected to change frequently, then the Factory Method Pattern may just increase the codebase without adding real value. (Over-engineering)
Conclusion
In software, things change fast. So, we need ways to update our apps without starting from scratch.
The Factory Method Pattern helps us do this. It's like having building blocks we can easily swap out and replace as needed.
It makes adding new things or changing parts of our app simpler.
However, it's important to understand the pros and cons.
Always prioritize simplicity and clarity, and employ patterns where they genuinely add value.
Thank you for reading, and in our next article we will discuss a little bit about another design pattern.
Feel free to comment, make suggestions and criticize.
See you :)
Top comments (0)