Home > Web Front-end > JS Tutorial > Mastering the Abstract Factory Pattern: A Comprehensive Guide

Mastering the Abstract Factory Pattern: A Comprehensive Guide

Susan Sarandon
Release: 2024-12-05 20:41:14
Original
906 people have browsed it

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

Copy after login
Copy after login
Copy after login
Copy after login
Copy after login
Copy after login

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
}

Copy after login
Copy after login
Copy after login
Copy after login
Copy after login

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

Copy after login
Copy after login
Copy after login
Copy after login
Copy after login
Copy after login

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
}

Copy after login
Copy after login
Copy after login
Copy after login
Copy after login

Structure

Mastering the Abstract Factory Pattern: A Comprehensive Guide

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:

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

Copy after login
Copy after login
Copy after login
Copy after login
Copy after login
Copy after login

Declaring Types

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
}

Copy after login
Copy after login
Copy after login
Copy after login
Copy after login
  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

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

Copy after login
Copy after login
Copy after login

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

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

Copy after login
Copy after login
Copy after login

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

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

Copy after login
Copy after login
Copy after login
Copy after login
Copy after login
Copy after login
  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

export interface ResumeFactory {
  createPDFResume(): PDFResume
  createMarkdownResume(): MarkdownResume
  createJSONResume(): JSONResume
}

Copy after login
Copy after login
Copy after login
Copy after login
Copy after login

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

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

Copy after login
Copy after login
Copy after login
  • The CreativeResumeFactory factory methods return the creative concrete product variant for every type of product.

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

Copy after login
Copy after login
Copy after login
  • The MinimalistResumeFactory factory methods return the minimalist concrete product variant for every type of product.

factories/ModernResumeFactory

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

Copy after login
Copy after login
Copy after login
Copy after login
Copy after login
Copy after login
  • 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

export interface ResumeFactory {
  createPDFResume(): PDFResume
  createMarkdownResume(): MarkdownResume
  createJSONResume(): JSONResume
}

Copy after login
Copy after login
Copy after login
Copy after login
Copy after login

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

Copy after login
Copy after login
Copy after login

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

Copy after login
Copy after login
Copy after login

The Minimalist Resume Factory Concrete Products

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

PDF Resume :

resumes/pdf/MinimalistPDFResume

npm install
npm start

Copy after login

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
}

Copy after login

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
}

Copy after login

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

Copy after login

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

Copy after login

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

Copy after login

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

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

Copy after login

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

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

Copy after login
Copy after login
Copy after login
Copy after login
Copy after login
Copy after login

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!

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!

source:dev.to
Statement of this Website
The content of this article is voluntarily contributed by netizens, and the copyright belongs to the original author. This site does not assume corresponding legal responsibility. If you find any content suspected of plagiarism or infringement, please contact admin@php.cn
Latest Articles by Author
Popular Tutorials
More>
Latest Downloads
More>
Web Effects
Website Source Code
Website Materials
Front End Template