Sitemap

Factory Method Pattern Implementation Using TypeScript and Node.js

6 min readAug 22, 2023

--

Press enter or click to view image in full size

The Factory Method Pattern offers an elegant solution to a common dilemma: how to create objects when the specific class to instantiate is not known in advance.

In this article, we will explore the purpose, motivation, application, and consequences of this pattern, along with implementation details in TypeScript and Node.js.

Purpose of the Factory Method Pattern

The purpose of the Factory Method Pattern is to define an interface for creating objects, delegating the responsibility of determining which specific class to instantiate to subclasses.

This allows for greater flexibility and decouples object creation from the classes that use them.

Motivation and Scenario

In a software development context, such as a framework, where various documents are presented to the user, the Factory Method Pattern becomes valuable.

Let’s imagine a drawing application where multiple types of documents exist. The Application class is responsible for managing documents and creating instances of them when needed.

However, as each document might have a specific subclass, the Application class cannot anticipate which Document subclass to create.

The Factory Method Pattern is useful when

  • A class cannot predict the class of objects it should create.
  • Subclasses are meant to specify the objects the main class creates.
  • The responsibility of object creation is delegated to auxiliary classes.

Participants

  • Product (Document): Defines the interface of objects created by the factory method.
  • Concrete Product (MyDocument): Implements the interface of the Product.
  • Creator (Application): Declares the factory method that returns a Product object. It can have a default implementation.
  • Concrete Creator (MyApplication): Redefines the factory method to create instances of a Concrete Product.

Consequences

The Factory Method Pattern has several consequences that impact the structure, flexibility, and maintainability of a software system. Let’s explore these consequences in more detail:

Hooks for Subclasses

One of the most notable advantages of the Factory Method Pattern is that it provides hooks for subclasses.

This means that subclasses can extend or modify the object creation behavior without altering the structure of the main class. By redefining the factory method in subclasses, they can provide an extended or adapted version of the created object.

This promotes extensibility and allows subclasses to tailor object creation to their specific needs.

Example: In the context of a graphic editing framework, a Document subclass could redefine the factory method to create a custom file dialog when opening an existing document. This demonstrates how the Factory Method Pattern enables subclasses to enrich the object creation process.

Connecting Parallel Class Hierarchies

Another interesting consequence of the Factory Method Pattern is that it facilitates the connection between parallel class hierarchies.

This occurs when a class delegates part of its responsibility to another class. For instance, in the case of manipulable graphic figures, one class hierarchy can manage interactions, while another handles the figures themselves.

The Factory Method Pattern acts as a bridge between these hierarchies, allowing classes in one hierarchy to create objects from the other hierarchy in a flexible manner.

Example: A Figure class could have a factory method to create a specific Manipulator for that figure. Subclasses of Figure would redefine this method to return the appropriate subclass of Manipulator. This illustrates how the Factory Method Pattern connects collaborating class hierarchies.

Flexibility and Adaptability

By separating object creation from the classes that use them, the Factory Method Pattern enhances system flexibility. It enables the addition of new product classes without altering existing code.

If a new type of Product is needed, simply create a subclass of ConcreteProduct and define its creation in the corresponding factory method within the Creator.

Decoupling and Maintainability

The Factory Method Pattern also contributes to decoupling system components.

The class that requires an object doesn’t need to know the specific concrete classes of those objects or how they are created.

This eases system modification and maintenance, as changes in object creation are encapsulated within the Creator’s subclasses and do not impact other parts of the code.

Reusable Code

Since the Factory Method Pattern promotes encapsulation and class hierarchy, code can become more modular and reusable.

Specific object creation implementations are maintained in concrete subclasses, enabling code reuse across different contexts.

Implementation

When implementing the Factory Method Pattern in TypeScript with Node.js, consider these aspects:

  1. Primary Approaches: Choose between using an abstract Creator class or a concrete Creator class with a default implementation.
  2. Parameterized Factory Methods: Leverage TypeScript’s type system to create factory methods that can produce different types of products based on input parameters.
  3. Language-Specific Considerations: TypeScript brings its own features to the pattern. For instance, interfaces and classes play a significant role in defining and implementing factory methods.
  4. Templates for Enhanced Flexibility: While TypeScript doesn’t directly support templates like C++, you can use generic types to achieve a similar level of flexibility in choosing product types without inheriting from the Creator.
  5. Naming Conventions for Clarity: Maintain clear naming conventions that indicate when factory methods are being employed, promoting readability and maintainability.

Example Code

In this scenario, let’s consider a logging system for an application that can log messages to different destinations, such as a console or a file.

Get Diego Motta | Fullstack Developer’s stories in your inbox

Join Medium for free to get updates from this writer.

We’ll use the Factory Method Pattern to create different types of loggers based on the chosen destination.

npm init -y
npm install typescript --save-dev
npx tsc --init

Directory structure

logging-factory/
├── ConsoleLogger.ts
├── FileLogger.ts
├── ILogger.ts
├── index.ts
├── LoggerFactory.ts
├── logs.txt
├── node_modules/
├── package.json
├── package-lock.json
├── tsconfig.json
└── yarn.lock
  • ILogger.ts: Interface for the logger classes.
  • ConsoleLogger.ts: Implementation of a logger that logs messages to the console.
  • FileLogger.ts: Implementation of a logger that logs messages to a file.
  • LoggerFactory.ts: Factory class responsible for creating logger instances.
  • index.ts: Main entry point of the application where you use the factory to create loggers.
  • logs.txt: Sample log file where file logs will be written.
  • node_modules/: Directory containing installed npm packages.
  • package.json: Package configuration file.
  • package-lock.json / yarn.lock: Dependency lock files.
  • tsconfig.json: TypeScript configuration file.

This structure should help you set up a basic experimental project to explore the Factory Method Pattern with TypeScript and Node.js for the logging system.

Define Logger Interfaces and Classes

// ILogger.ts
export interface ILogger {
log(message: string): void;
}

// ConsoleLogger.ts
import { ILogger } from "./ILogger";

export class ConsoleLogger implements ILogger {
log(message: string): void {
console.log(`[Console] ${message}`);
}
}

// FileLogger.ts
import { ILogger } from "./ILogger";
import * as fs from "fs";

export class FileLogger implements ILogger {
private readonly filePath: string;

constructor(filePath: string) {
this.filePath = filePath;
}

log(message: string): void {
fs.appendFileSync(this.filePath, `[File] ${message}\n`);
}
}

Create a Logger Factory

// LoggerFactory.ts
import { ILogger } from "./ILogger";
import { ConsoleLogger } from "./ConsoleLogger";
import { FileLogger } from "./FileLogger";

export enum LoggerType {
Console,
File
}

export class LoggerFactory {
static createLogger(type: LoggerType, options?: any): ILogger {
switch (type) {
case LoggerType.Console:
return new ConsoleLogger();
case LoggerType.File:
if (options && options.filePath) {
return new FileLogger(options.filePath);
} else {
throw new Error("File path is missing for FileLogger.");
}
default:
throw new Error("Invalid logger type.");
}
}
}

Using the Factory

// index.ts
import { LoggerFactory, LoggerType } from "./LoggerFactory";

const consoleLogger = LoggerFactory.createLogger(LoggerType.Console);
consoleLogger.log("This is a console log.");

const fileLogger = LoggerFactory.createLogger(LoggerType.File, { filePath: "logs.txt" });
fileLogger.log("This is a file log.");

Compile and Run

Compile your TypeScript code using tsc:

npx tsc

Run your application using Node.js:

node index.js

Conclusion

We’ve created a simple logging system that can create different types of loggers (ConsoleLogger and FileLogger) using the Factory Method Pattern.

The LoggerFactory class acts as the factory, responsible for creating instances of different logger types based on the given parameters.

This demonstrates how the Factory Method Pattern can help create objects with different behaviors based on specific requirements.

References

  1. Gamma, E., Helm, R., Johnson, R., & Vlissides, J. (1994). Design Patterns: Elements of Reusable Object-Oriented Software. Addison-Wesley.
  2. Dive Into Design Patterns, Retrieved from https://refactoring.guru/design-patterns/book

🧑‍💻 I’m Diego. As a Fullstack Developer with over 9 years of experience in the IT industry.

Let’s Connect!

📢 Follow me on social media for more insights, updates, and discussions:

🐦 My Twitter/X

💼 My LinkedIn

🏆My work achievements

I love sharing about:

📚Books

💻 Web Development

🌐 JavaScript, React & Node

🛠️ Tips for IT Interviews

See you!

--

--

Diego Motta | Fullstack Developer
Diego Motta | Fullstack Developer

Hi!👋 I'm a web developer with over 9 years of experience in the IT industry, eager to share skills and experiences with fellow devs