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.
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.
The Abstract Factory design pattern solves the following problems:
The Abstract Factory design pattern solves these problems by declaring an interface or abstract class for each type of product.
export abstract class PDFResume {} export abstract class JSONResume {} export abstract class MarkdownResume {}
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:
export interface ResumeFactory { createPDFResume(): PDFResume createMarkdownResume(): MarkdownResume createJSONResume(): JSONResume }
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 ).
export abstract class PDFResume {} export abstract class JSONResume {} export abstract class MarkdownResume {}
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:
export interface ResumeFactory { createPDFResume(): PDFResume createMarkdownResume(): MarkdownResume createJSONResume(): JSONResume }
The structure of the Abstract Factory design pattern consists of the following classes:
In our case, the factory methods declared in Factory are: createProductA and createProductB
ConcretProductA1 and ConcretProductA2 implement IProductA ConcretProductB1 and ConcretProductB2 implement IProductB
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:
export abstract class PDFResume {} export abstract class JSONResume {} export abstract class MarkdownResume {}
Let's start by declaring the types which we will be using throughout the tutorial to ensure type safety.
interfaces/Types
export interface ResumeFactory { createPDFResume(): PDFResume createMarkdownResume(): MarkdownResume createJSONResume(): JSONResume }
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
export class CreativeResumeFactory implements ResumeFactory { createPDFResume(): CreativePDFResume { return new CreativePDFResume() // CreativePDFResume implements PDFResume } createMarkdownResume(): CreativeMarkdownResume { return new CreativeMarkdownResume() // CreativeMarkdownResume implements MarkdownResume } createJSONResume(): CreativeJSONResume { return new CreativeJSONResume() // CreativeJSONResume implements JSONResume } }
We will be going through their code in the next section.
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.
The class defines:
resumes/json/JSONResume
// User inputs... let theme = "minimalist" let format = "pdf" let factory: ResumeFactory switch (theme) { case "minimalist": factory = new MinimalistResumeFactory() break case "modern": factory = new ModernResumeFactory() break case "creative": factory = new CreativeResumeFactory() break default: throw new Error("Invalid theme.") } const userInput = await getUserInput() let resume switch (format) { case "pdf": resume = factory.createPDFResume() break case "markdown": resume = factory.createMarkdownResume() break case "json": resume = factory.createJSONResume() break default: throw new Error("Invalid format.") }
The keyword abstract means that the class is a generic type which can't be instantiated; it can only be inherited by other classes.
The class defines:
resumes/markdown/MarkdownResume
export abstract class PDFResume {} export abstract class JSONResume {} export abstract class MarkdownResume {}
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:
resumes/pdf/PDFResume
export interface ResumeFactory { createPDFResume(): PDFResume createMarkdownResume(): MarkdownResume createJSONResume(): JSONResume }
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:
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:
factories/CreativeResumeFactory
export class CreativeResumeFactory implements ResumeFactory { createPDFResume(): CreativePDFResume { return new CreativePDFResume() // CreativePDFResume implements PDFResume } createMarkdownResume(): CreativeMarkdownResume { return new CreativeMarkdownResume() // CreativeMarkdownResume implements MarkdownResume } createJSONResume(): CreativeJSONResume { return new CreativeJSONResume() // CreativeJSONResume implements JSONResume } }
factories/MinimalistResumeFactory
// User inputs... let theme = "minimalist" let format = "pdf" let factory: ResumeFactory switch (theme) { case "minimalist": factory = new MinimalistResumeFactory() break case "modern": factory = new ModernResumeFactory() break case "creative": factory = new CreativeResumeFactory() break default: throw new Error("Invalid theme.") } const userInput = await getUserInput() let resume switch (format) { case "pdf": resume = factory.createPDFResume() break case "markdown": resume = factory.createMarkdownResume() break case "json": resume = factory.createJSONResume() break default: throw new Error("Invalid format.") }
factories/ModernResumeFactory
export abstract class PDFResume {} export abstract class JSONResume {} export abstract class MarkdownResume {}
Now, let's create the previous ConcreteProducts which are returned by the CreativeResumeFactory
PDF Resume :
resumes/pdf/CreativePDFResume
export interface ResumeFactory { createPDFResume(): PDFResume createMarkdownResume(): MarkdownResume createJSONResume(): JSONResume }
Markdown Resume :
resumes/markdown/CreativeMarkdownResume
export class CreativeResumeFactory implements ResumeFactory { createPDFResume(): CreativePDFResume { return new CreativePDFResume() // CreativePDFResume implements PDFResume } createMarkdownResume(): CreativeMarkdownResume { return new CreativeMarkdownResume() // CreativeMarkdownResume implements MarkdownResume } createJSONResume(): CreativeJSONResume { return new CreativeJSONResume() // CreativeJSONResume implements JSONResume } }
JSON Resume :
resumes/json/CreativeJSONResume
// User inputs... let theme = "minimalist" let format = "pdf" let factory: ResumeFactory switch (theme) { case "minimalist": factory = new MinimalistResumeFactory() break case "modern": factory = new ModernResumeFactory() break case "creative": factory = new CreativeResumeFactory() break default: throw new Error("Invalid theme.") } const userInput = await getUserInput() let resume switch (format) { case "pdf": resume = factory.createPDFResume() break case "markdown": resume = factory.createMarkdownResume() break case "json": resume = factory.createJSONResume() break default: throw new Error("Invalid format.") }
Next, let's create the previous ConcreteProducts which are returned by the MinimalistResumeFactory
PDF Resume :
resumes/pdf/MinimalistPDFResume
npm install npm start
Markdown Resume :
resumes/markdown/MinimalistMarkdownResume
export type ResumeData = { name: string email: string phone: string experience: Experience[] } export type Experience = { company: string position: string startDate: string endDate: string description: string }
JSON Resume :
resumes/json/MinimalistJSONResume
import { JSONResume } from "../resumes/json/JSONResume" import { MarkdownResume } from "../resumes/markdown/MarkdownResume" import { PDFResume } from "../resumes/pdf/PdfResume" export interface ResumeFactory { createPDFResume(): PDFResume createMarkdownResume(): MarkdownResume createJSONResume(): JSONResume }
Finally, let's create the previous ConcreteProducts which are returned by the ModernResumeFactory
PDF Resume :
resumes/pdf/ModernPDFResume
import * as fs from "fs/promises" import { ResumeData } from "../../interfaces/Types" export abstract class JSONResume { protected data!: ResumeData & { style: string } abstract generate(data: ResumeData): void async saveToFile(fileName: string): Promise<void> { await fs.writeFile(fileName, JSON.stringify(this.data, null, 2)) } getData(): any { return this.data } }
Markdown Resume :
resumes/markdown/ModernMarkdownResume
import * as fs from "fs/promises" import { ResumeData } from "../../interfaces/Types" export abstract class MarkdownResume { protected content: string = "" abstract generate(data: ResumeData): void async saveToFile(fileName: string): Promise<void> { await fs.writeFile(fileName, this.content) } getContent(): string { return this.content } }
JSON Resume :
resumes/json/ModernJSONResume
import * as fs from "fs" import PDFDocument from "pdfkit" import { ResumeData } from "../../interfaces/Types" export abstract class PDFResume { protected doc: PDFKit.PDFDocument constructor() { this.doc = new PDFDocument() } abstract generate(data: ResumeData): void async saveToFile(fileName: string): Promise<void> { const stream = fs.createWriteStream(fileName) this.doc.pipe(stream) this.doc.end() await new Promise<void>((resolve, reject) => { stream.on("finish", resolve) stream.on("error", reject) }) } getBuffer(): Buffer { return this.doc.read() as Buffer } }
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:
index.ts
import { ResumeFactory } from "../interfaces/ResumeFactory" import { CreativeJSONResume } from "../resumes/json/CreativeJSONResume" import { CreativeMarkdownResume } from "../resumes/markdown/CreativeMarkdownResume" import { CreativePDFResume } from "../resumes/pdf/CreativePDFResume" export class CreativeResumeFactory implements ResumeFactory { createPDFResume(): CreativePDFResume { return new CreativePDFResume() // CreativePDFResume extends PDFResume } createMarkdownResume(): CreativeMarkdownResume { return new CreativeMarkdownResume() // CreativeMarkdownResume extends MarkdownResume } createJSONResume(): CreativeJSONResume { return new CreativeJSONResume() // CreativeJSONResume extends JSONResume } }
The code above works in three steps:
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.
utils/userInput
export abstract class PDFResume {} export abstract class JSONResume {} export abstract class MarkdownResume {}
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:
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.
If you have any questions or want to discuss something further, feel free to Contact me here.
Happy coding!
The above is the detailed content of Mastering the Abstract Factory Pattern: A Comprehensive Guide. For more information, please follow other related articles on the PHP Chinese website!