Have you ever found yourself needing to create multiple variations of different families of object in your application without duplicating the logic over and over?
Or perhaps you’ve built an application, only to realize that new requirements or a client’s changed preferences demand entirely new objects, forcing you to rework your entire codebase?
What if there was a way to seamlessly introduce new variations without breaking your existing code just by plugging in a new implementation?
That’s where the Abstract Factory design pattern comes in!
In this tutorial, we’ll break down this powerful design pattern by building a Node.js CLI application for creating mutiple types of resumes supporting multiples formats and themes.
Overview
The Abstract Factory is a creational design pattern, which is a category of design patterns that deals with the different problems that come with the native way of creating objects using the new keyword or operator.
You can think of the Abstract Factory design pattern as a generalization of the factory method design pattern which we've covered in this blog article.
Problem
The Abstract Factory design pattern solves the following problems:
- How can we create families of related products such as: PDFResume, JSONResume, and MarkdownResume?
- How can we support having multiple variants per product family such as: CreativeResume, MinimalistResume, and ModernResume?
- How can we support adding more variants and products without breaking our existing consuming or client code?
Solution
The Abstract Factory design pattern solves these problems by declaring an interface or abstract class for each type of product.
And then, as the name of the pattern implies, we create an abstract factory which is an interface that declares factory methods that create every type of product:
- createPDFResume: which returns a PDFResume type or subtype.
- createMarkdownResume: which returns a MarkdownResume type or subtype.
- createJSONResume: which returns a JSONResume type or subtype.
Okay, now we have a generic factory which returns every possible type of product, but how can we support multiple variants per product?
The answer is by creating a ConcreteFactory which implements the abstract factory (ResumeFactory).
Now, to consume our factories in our client class, we just have to declare a variable of type ResumeFactory and then instantiate the corresponding Concrete factory depending on the user input.
Client code:
Structure
The structure of the Abstract Factory design pattern consists of the following classes:
- Factory: The reason for naming this design pattern abstract factory is that this class represents the contract between all the ConcreteFactories. It defines all the factory methods.
- The number of factory methods is equal to the number of products.
- Each factory method should return an abstract or generic product type (IProduct{j}).
In our case, the factory methods declared in Factory are: createProductA and createProductB
- ConcreteFactory{i}: These classes implement the Factory class and provide custom implementations for each factory method.
- In the above schema, i is equal to either 1 or 2.
- The number of ConcreteFactories is equal to the number of possible variants per product.
- Each concrete factory method should return an object which is an instance of the corresponding product.
- IProduct{j}: These classes correspond to the abstract product types.
- In the above schema, j is equal to either A or B.
- Each IProduct{j} is implemented by many concrete product classes.
ConcretProductA1 and ConcretProductA2 implement IProductA ConcretProductB1 and ConcretProductB2 implement IProductB
- ConcreteProducts are the products which implement one of the IProduct{j} generic types.
Practical Scenario
In this section, we are going to put the previous example into action by building a fully working Node.js TypeScript CLI Application which creates a resume based on the chosen theme and format by the user.
Feel free to check out the full working code by cloning this repository on your machine.
Then run the following commands:
Declaring Types
Let's start by declaring the types which we will be using throughout the tutorial to ensure type safety.
interfaces/Types
- The ResumeData type defines all the attributes of a resume object such as: name, email, phone, and an array of experiences.
- The Experience type consists of the: company, position, startDate, endDate, and description.
Declaring Our Abstract Factory
Now, let's declare the generic factory type, which will be defining the three factory methods which correspond to the different supported product types: PDFResume, MarkdownResume, and JSONResume.
interfaces/ResumeFactory
We will be going through their code in the next section.
Declaring The Shared Class for the Different Types of Documents
Next, let's move on to creating our generic product classes.
Every product type will be an abstract class because we want to share both attributes and methods between their corresponding subtypes.
- JSONResume: The class has a protected data attribute, storing an object of type ResumeData with an extra attribute called style.
The class defines:
- A getter method to access the data attribute.
- An abstract generate method which will be overridden by the subclasses later.
- A saveToFile method with a basic implementation, which consists of storing the resume data in a JSON file.
resumes/json/JSONResume
The keyword abstract means that the class is a generic type which can't be instantiated; it can only be inherited by other classes.
- MarkdownResume: The class has a protected content attribute, storing the markdown string.
The class defines:
- A getter method to access the content attribute.
- An abstract generate method which will be overridden by the subclasses later.
- A saveToFile method which takes a fileName and then stores the markdown formatted string content into a file.
resumes/markdown/MarkdownResume
- PDFResume:
The class has a protected doc object of type PDFKit.PDFDocument, which is imported from a library called pdfkit. The library simplifies creating and manipulating PDF documents through its object-oriented interface.
The class defines:
- A getter method to access the doc attribute.
- An abstract generate method which will be overridden by the subclasses later.
- A saveToFile method which saves the doc in-memory PDF object into a specific file.
resumes/pdf/PDFResume
Declaring our Concrete Factories
Now that we've defined our generic product types and our abstract factory, it's time to proceed with the creation of our ConcreteFactories which correspond to the different variants of every generic product type.
We have 3 possible variants for a resume: Creative, Minimalist, and Modern. And 3 types of generic Products: JSON, PDF, and Markdown.
The abstract factory (ResumeFactory) defines the 3 factory methods which are responsible for creating our products:
- createPDFResume: creates an instance of type PDFResume.
- createMarkdownResume: creates an instance of type MarkdownResume.
- createJSONResume: creates an instance of type JSONResume.
To support multiple variants per product, we will have to create 3 concrete factories.
Each Concrete factory will be creating the 3 types of products but with its own flavors:
- CreativeResumeFactory creates products of the Creative variant.
- MinimalistResumeFactory creates products of the Minimalist variant.
- ModernResumeFactory creates products of the Modern variant.
factories/CreativeResumeFactory
- The CreativeResumeFactory factory methods return the creative concrete product variant for every type of product.
factories/MinimalistResumeFactory
- The MinimalistResumeFactory factory methods return the minimalist concrete product variant for every type of product.
factories/ModernResumeFactory
- The ModernResumeFactory factory methods return the modern concrete product variant for every type of product.
The Creative Resume Factory Concrete Products
Now, let's create the previous ConcreteProducts which are returned by the CreativeResumeFactory
PDF Resume:
resumes/pdf/CreativePDFResume
Markdown Resume:
resumes/markdown/CreativeMarkdownResume
JSON Resume:
resumes/json/CreativeJSONResume
The Minimalist Resume Factory Concrete Products
Next, let's create the previous ConcreteProducts which are returned by the MinimalistResumeFactory
PDF Resume:
resumes/pdf/MinimalistPDFResume
Markdown Resume:
resumes/markdown/MinimalistMarkdownResume
JSON Resume:
resumes/json/MinimalistJSONResume
The Modern Resume Factory Concrete Products
Finally, let's create the previous ConcreteProducts which are returned by the ModernResumeFactory
PDF Resume:
resumes/pdf/ModernPDFResume
Markdown Resume:
resumes/markdown/ModernMarkdownResume
JSON Resume:
resumes/json/ModernJSONResume
Using Our Factories in our Index.ts File
Let's start bearing the fruits of our previous work by using our factories in the client code.
Look how we can now consume our resume builder library in a very clean way by just using our factories.
The user only has to provide two things:
- The Product Type: What type of PDFs does he want to create?
- The theme: What kind of resume styles does he prefer?
index.ts
The code above works in three steps:
- User Inputs: We first get the theme and format values.
- Choosing A factory: Then we instantiate the corresponding factory based on the theme value.
- Creating the Product: Finally, we call the corresponding factory method depending on the chosen format.
The user doesn't care about how products and their corresponding variants are created; they only need to select a theme and format, and that's it - the corresponding product gets created as requested.
The client code is now robust for changes. If we want to add a new theme or style, we can just create a new factory which is responsible for doing so.
We've used the chalk library to color our terminal logs depending on their semantic meaning.
To be able to get the inputs from the CLI app's user, we've used the inquirer package, which provides a really appealing and user-friendly way to get various types of inputs from the user.
- The getUserInput function was used to get the main resume information: name, email, phone.
- The getExperience utility function was used to recursively retrieve the experience information from the user. In other words, it prompts the user to fill in the experience information for the first entry, then asks if they have another experience to add. If the answer is no, the function just returns; on the other hand, if they select yes, they will be asked again to fill in the next experience's information.
utils/userInput
Conclusion
The Abstract Factory pattern is a powerful tool in the arsenal of software designers and developers. It provides a structured approach to creating families of related objects without specifying their concrete classes. This pattern is particularly useful when:
- A system should be independent of how its products are created, composed, and represented.
- A system needs to be configured with one of multiple families of products.
- A family of related product objects is designed to be used together, and you need to enforce this constraint.
- You want to provide a class library of products, and you want to reveal just their interfaces, not their implementations.
In our practical example, we've seen how the Abstract Factory pattern can be applied to create a flexible and extensible resume generation system. This system can easily accommodate new resume styles or output formats without modifying the existing code, demonstrating the power of the Open/Closed Principle in action.
While the Abstract Factory pattern offers many benefits, it's important to note that it can introduce additional complexity to your codebase. Therefore, it's crucial to assess whether the flexibility it provides is necessary for your specific use case.
By mastering design patterns like the Abstract Factory, you'll be better equipped to create robust, flexible, and maintainable software systems. Keep exploring and applying these patterns in your projects to elevate your software design skills.
Contact
If you have any questions or want to discuss something further, feel free to Contact me here.
Happy coding!