DEV Community

Cover image for Mastering the Abstract Factory Pattern: A Comprehensive Guide
Sidali Assoul
Sidali Assoul

Posted on • Originally published at spithacode.com on

Mastering the Abstract Factory Pattern: A Comprehensive Guide

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:

  1. How can we create families of related products such as: PDFResume , JSONResume , and MarkdownResume?
  2. How can we support having multiple variants per product family such as: CreativeResume , MinimalistResume , and ModernResume?
  3. 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.

export abstract class PDFResume {}
export abstract class JSONResume {}
export abstract class MarkdownResume {}

Enter fullscreen mode Exit fullscreen mode

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.
export interface ResumeFactory {
  createPDFResume(): PDFResume
  createMarkdownResume(): MarkdownResume
  createJSONResume(): JSONResume
}

Enter fullscreen mode Exit fullscreen mode

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 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
  }
}

Enter fullscreen mode Exit fullscreen mode

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:

// 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.")
}

Enter fullscreen mode Exit fullscreen mode

Structure

Abstract Factory Design Pattern Structure

The structure of the Abstract Factory design pattern consists of the following classes:

  1. 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

  1. 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.
  1. 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

  1. 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:

npm install
npm start

Enter fullscreen mode Exit fullscreen mode

Declaring Types

Let's start by declaring the types which we will be using throughout the tutorial to ensure type safety.

interfaces/Types

export type ResumeData = {
  name: string
  email: string
  phone: string
  experience: Experience[]
}

export type Experience = {
  company: string
  position: string
  startDate: string
  endDate: string
  description: string
}

Enter fullscreen mode Exit fullscreen mode
  1. The ResumeData type defines all the attributes of a resume object such as: name, email, phone, and an array of experiences.
  2. 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

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
}

Enter fullscreen mode Exit fullscreen mode

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.

  1. 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

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
  }
}

Enter fullscreen mode Exit fullscreen mode

The keyword abstract means that the class is a generic type which can't be instantiated; it can only be inherited by other classes.

  1. 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

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
  }
}

Enter fullscreen mode Exit fullscreen mode
  1. 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

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
  }
}

Enter fullscreen mode Exit fullscreen mode

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:

  1. CreativeResumeFactory creates products of the Creative variant.
  2. MinimalistResumeFactory creates products of the Minimalist variant.
  3. ModernResumeFactory creates products of the Modern variant.

factories/CreativeResumeFactory

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
  }
}

Enter fullscreen mode Exit fullscreen mode
  • The CreativeResumeFactory factory methods return the creative concrete product variant for every type of product.

factories/MinimalistResumeFactory

import { ResumeFactory } from "../interfaces/ResumeFactory"
import { MinimalistJSONResume } from "../resumes/json/MinimalistJSONResume"
import { MinimalistMarkdownResume } from "../resumes/markdown/MinimalistMarkdownResume"
import { MinimalistPDFResume } from "../resumes/pdf/MinimalistPDFResume"

export class MinimalistResumeFactory implements ResumeFactory {
  createPDFResume(): MinimalistPDFResume {
    return new MinimalistPDFResume() // extends PDFResume
  }

  createMarkdownResume(): MinimalistMarkdownResume {
    return new MinimalistMarkdownResume() // extends MarkdownResume
  }

  createJSONResume(): MinimalistJSONResume {
    return new MinimalistJSONResume() // extends JSONResume
  }
}

Enter fullscreen mode Exit fullscreen mode
  • The MinimalistResumeFactory factory methods return the minimalist concrete product variant for every type of product.

factories/ModernResumeFactory

import { ResumeFactory } from "../interfaces/ResumeFactory"
import { ModernJSONResume } from "../resumes/json/ModernJSONResume"
import { ModernMarkdownResume } from "../resumes/markdown/ModernMarkdownResume"
import { ModernPDFResume } from "../resumes/pdf/ModernPDFResume"

export class ModernResumeFactory implements ResumeFactory {
  createPDFResume(): ModernPDFResume {
    return new ModernPDFResume() // extends PDFResume
  }

  createMarkdownResume(): ModernMarkdownResume {
    return new ModernMarkdownResume() // extends MarkdownResume
  }

  createJSONResume(): ModernJSONResume {
    return new ModernJSONResume() // extends JSONResume
  }
}

Enter fullscreen mode Exit fullscreen mode
  • 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

import { ResumeData } from "../../interfaces/Types"
import { PDFResume } from "./PdfResume"

export class CreativePDFResume extends PDFResume {
  generate(data: ResumeData): void {
    this.doc.rect(0, 0, 200, this.doc.page.height).fill("#FFD700")
    this.doc.fill("black").fontSize(28).text(data.name, 220, 50)
    this.doc.fontSize(12).text(`\${data.email} | \${data.phone}`, 220, 80)
    this.doc.fontSize(16).text("Experience", 220, 120)
    let yPos = 140
    data.experience.forEach((exp: any) => {
      this.doc.fontSize(14).text(exp.company, 220, yPos)
      this.doc
        .fontSize(12)
        .text(
          `\${exp.position} (\${exp.startDate} - \${exp.endDate})`,
          220,
          yPos + 20
        )
      this.doc
        .fontSize(10)
        .text(exp.description, 220, yPos + 40, { width: 350 })
      yPos += 80
    })
  }
}

Enter fullscreen mode Exit fullscreen mode

Markdown Resume :

resumes/markdown/CreativeMarkdownResume

import { ResumeData } from "../../interfaces/Types"
import { MarkdownResume } from "./MarkdownResume"

export class CreativeMarkdownResume extends MarkdownResume {
  generate(data: ResumeData): void {
    this.content = `# 🌟 \${data.name} 🌟\n\n`
    this.content += `📧 \${data.email} | 📞 \${data.phone}\n\n`
    this.content += "## 💼 Career Journey\n\n"
    data.experience.forEach((exp: any) => {
      this.content += `### 🏢 \${exp.company}\n\n`
      this.content += ` **\${exp.position}** | \${exp.startDate} - \${exp.endDate}\n\n`
      this.content += `\${exp.description}\n\n`
    })
  }
}

Enter fullscreen mode Exit fullscreen mode

JSON Resume :

resumes/json/CreativeJSONResume

import { ResumeData } from "../../interfaces/Types"
import { JSONResume } from "./JSONResume"

export class CreativeJSONResume extends JSONResume {
  generate(data: ResumeData): void {
    this.data = {
      style: "creative",
      ...data,
    }
  }
}

Enter fullscreen mode Exit fullscreen mode

The Minimalist Resume Factory Concrete Products

Next, let's create the previous ConcreteProducts which are returned by the MinimalistResumeFactory

PDF Resume :

resumes/pdf/MinimalistPDFResume

import { ResumeData } from "../../interfaces/Types"
import { PDFResume } from "./PdfResume"

export class MinimalistPDFResume extends PDFResume {
  generate(data: ResumeData): void {
    this.doc.fontSize(24).text(data.name, { align: "center" })
    this.doc
      .fontSize(12)
      .text(`Email: \${data.email} | Phone: \${data.phone}`, {
        align: "center",
      })
    this.doc.moveDown()
    this.doc.fontSize(16).text("Experience")
    data.experience.forEach((exp: any) => {
      this.doc.fontSize(14).text(exp.company)
      this.doc
        .fontSize(12)
        .text(`\${exp.position} (\${exp.startDate} - \${exp.endDate})`)
      this.doc.fontSize(10).text(exp.description)
      this.doc.moveDown()
    })
  }
}

Enter fullscreen mode Exit fullscreen mode

Markdown Resume :

resumes/markdown/MinimalistMarkdownResume

import { ResumeData } from "../../interfaces/Types"
import { MarkdownResume } from "./MarkdownResume"

export class MinimalistMarkdownResume extends MarkdownResume {
  generate(data: ResumeData): void {
    this.content = `# \${data.name}\n\n`
    this.content += `Email: \${data.email} | Phone: \${data.phone}\n\n`
    this.content += "## Experience\n\n"
    data.experience.forEach((exp: any) => {
      this.content += `### \${exp.company}\n\n`
      this.content += `\${exp.position} (\${exp.startDate} - \${exp.endDate})\n\n`
      this.content += `\${exp.description}\n\n`
    })
  }
}

Enter fullscreen mode Exit fullscreen mode

JSON Resume :

resumes/json/MinimalistJSONResume

import { ResumeData } from "../../interfaces/Types"
import { JSONResume } from "./JSONResume"

export class MinimalistJSONResume extends JSONResume {
  generate(data: ResumeData): void {
    this.data = {
      style: "minimalist",
      ...data,
    }
  }
}

Enter fullscreen mode Exit fullscreen mode

The Modern Resume Factory Concrete Products

Finally, let's create the previous ConcreteProducts which are returned by the ModernResumeFactory

PDF Resume :

resumes/pdf/ModernPDFResume

import { ResumeData } from "../../interfaces/Types"
import { PDFResume } from "./PdfResume"

export class ModernPDFResume extends PDFResume {
  generate(data: ResumeData): void {
    this.doc.rect(0, 0, this.doc.page.width, 60).fill("#4A90E2")
    this.doc.fill("white").fontSize(28).text(data.name, 50, 20)
    this.doc.fontSize(12).text(`\${data.email} | \${data.phone}`, 50, 50)
    this.doc.fill("black").fontSize(16).text("Experience", 50, 80)
    let yPos = 100
    data.experience.forEach((exp: any) => {
      this.doc.fontSize(14).text(exp.company, 50, yPos)
      this.doc
        .fontSize(12)
        .text(
          `\${exp.position} (\${exp.startDate} - \${exp.endDate})`,
          50,
          yPos + 20
        )
      this.doc.fontSize(10).text(exp.description, 50, yPos + 40, { width: 500 })
      yPos += 80
    })
  }
}

Enter fullscreen mode Exit fullscreen mode

Markdown Resume :

resumes/markdown/ModernMarkdownResume

import { ResumeData } from "../../interfaces/Types"
import { MarkdownResume } from "./MarkdownResume"

export class ModernMarkdownResume extends MarkdownResume {
  generate(data: ResumeData): void {
    this.content = `# \${data.name}\n\n`
    this.content += ` **Email:** \${data.email} | **Phone:** \${data.phone}\n\n`
    this.content += "## Professional Experience\n\n"
    data.experience.forEach((exp: any) => {
      this.content += `### \${exp.company}\n\n`
      this.content += ` **\${exp.position}** | \${exp.startDate} - \${exp.endDate}\n\n`
      this.content += `\${exp.description}\n\n`
    })
  }
}

Enter fullscreen mode Exit fullscreen mode

JSON Resume :

resumes/json/ModernJSONResume

import { ResumeData } from "../../interfaces/Types"
import { JSONResume } from "./JSONResume"

export class ModernJSONResume extends JSONResume {
  generate(data: ResumeData): void {
    this.data = {
      style: "modern",
      ...data,
    }
  }
}

Enter fullscreen mode Exit fullscreen mode

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:

  1. The Product Type : What type of PDFs does he want to create?
  2. The theme : What kind of resume styles does he prefer?

index.ts

#!/usr/bin/env node
import chalk from "chalk"
import inquirer from "inquirer"

import { CreativeResumeFactory } from "./factories/CreativeResumeFactory"
import { MinimalistResumeFactory } from "./factories/MinimalistResumeFactory"
import { ModernResumeFactory } from "./factories/ModernResumeFactory"
import { ResumeFactory } from "./interfaces/ResumeFactory"
import { getUserInput } from "./utils/userInput"

async function main() {
  console.log(chalk.blue.bold("Welcome to the Theme-Based Resume Generator!"))

  const { theme, format } = await inquirer.prompt([
    {
      type: "list",
      name: "theme",
      message: "Choose a resume theme:",
      choices: ["minimalist", "modern", "creative"],
    },
    {
      type: "list",
      name: "format",
      message: "Choose an output format:",
      choices: ["pdf", "markdown", "json"],
    },
  ])

  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.")
  }

  console.log(chalk.yellow("Generating your resume..."))

  try {
    resume.generate(userInput)
    const fileName = `\${userInput.name.replace(/\s+/g, "_")}_\${theme}_resume.\${format}`
    await resume.saveToFile(fileName)
    console.log(chalk.green.bold("Resume generated successfully!"))
    console.log(
      chalk.cyan(
        `Your \${theme} \${format} resume has been saved as \${fileName}.`
      )
    )
  } catch (error) {
    console.error(
      chalk.red("Error generating resume:"),
      (error as Error).message
    )
  }
}

main().catch((error) => {
  console.error(chalk.red("An unexpected error occurred:"), error.message)
  process.exit(1)
})

Enter fullscreen mode Exit fullscreen mode

The code above works in three steps:

  1. User Inputs: We first get the theme and format values.
  2. Choosing A factory : Then we instantiate the corresponding factory based on the theme value.
  3. 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.

  1. The getUserInput function was used to get the main resume information: name, email, phone.
  2. 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

import inquirer from "inquirer"

import { Experience, ResumeData } from "../interfaces/Types"

export async function getUserInput(): Promise<ResumeData> {
  const questions = [
    {
      type: "input",
      name: "name",
      message: "Enter your full name:",
      validate: (input: string) => input.trim() !== "" || "Name is required",
    },
    {
      type: "input",
      name: "email",
      message: "Enter your email address:",
      validate: (input: string) =>
        /\S+@\S+\.\S+/.test(input) || "Please enter a valid email address",
    },
    {
      type: "input",
      name: "phone",
      message: "Enter your phone number:",
      validate: (input: string) =>
        input.trim() !== "" || "Phone number is required",
    },
  ]

  const { name, email, phone } = await inquirer.prompt<{
    name: string
    email: string
    phone: string
  }>(questions)

  const experience = await getExperience()

  return { name, email, phone, experience }
}

async function getExperience(): Promise<Experience[]> {
  const experience: Experience[] = []
  let addMore = true

  while (addMore) {
    const job = await inquirer.prompt<Experience>([
      {
        type: "input",
        name: "company",
        message: "Enter company name:",
        validate: (input: string) =>
          input.trim() !== "" || "Company name is required",
      },
      {
        type: "input",
        name: "position",
        message: "Enter your position:",
        validate: (input: string) =>
          input.trim() !== "" || "Position is required",
      },
      {
        type: "input",
        name: "startDate",
        message: "Enter start date (MM/YYYY):",
        validate: (input: string) =>
          /^\d{2}\/\d{4}$/.test(input) || "Please enter a valid date (MM/YYYY)",
      },
      {
        type: "input",
        name: "endDate",
        message: 'Enter end date (MM/YYYY or "Present"):',
        validate: (input: string) =>
          /^\d{2}\/\d{4}$/.test(input) ||
          input.toLowerCase() === "present" ||
          'Please enter a valid date (MM/YYYY) or "Present"',
      },
      {
        type: "input",
        name: "description",
        message: "Enter job description:",
        validate: (input: string) =>
          input.trim() !== "" || "Job description is required",
      },
    ])

    experience.push(job)

    const { continueAdding } = await inquirer.prompt<{
      continueAdding: boolean
    }>({
      type: "confirm",
      name: "continueAdding",
      message: "Do you want to add another job experience?",
      default: false,
    })

    addMore = continueAdding
  }

  return experience
}

Enter fullscreen mode Exit fullscreen mode

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:

  1. A system should be independent of how its products are created, composed, and represented.
  2. A system needs to be configured with one of multiple families of products.
  3. A family of related product objects is designed to be used together, and you need to enforce this constraint.
  4. 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!

Top comments (0)