Factory Design Pattern

The factory design pattern is a creational design pattern that provides an interface for creating objects in a superclass but allows subclasses to change the type of objects that will be created. Instead of calling the constructor directly, we use a factory method to create objects.

It helps you avoid tight coupling between your code and the specific classes you need. This makes it easier to change or add new types of objects without modifying the code that uses these objects.

Key Points:

The creation logic is encapsulated within factory methods.

Subclasses can decide which class to instantiate.

The creation process is separated from the business logic.

Real-World Scenario: API Clients for Different Data Formats

Let us assume we are building an app that interacts with various APIs.

Each API might use a different data format for requests and responses, such as JSON, XML, or YAML. We want a flexible way to create API clients that handle these different formats without cluttering the code with multiple constructors or conditional statements.

Different data formats require different handling when sending requests.Similarly, responses need to be parsed according to their format.The goal is to easily switch between different API clients based on the required format.

Let's implement the Factory Method pattern to handle JSON, XML, and YAML API clients.

Step 1: Define the Product Interface

Let's implement the Factory Method pattern to handle JSON, XML, and YAML API clients.

//ApiClient.ts
export interface ApiClient {
    sendRequest(endpoint: string, data: any): Promise<any>;
    parseResponse(response: any): any;
}

Step 2: Create Concrete Products

Create concrete classes (or products as per the factory design pattern jargon), that implement the ApiClient interface for each data format.

JSON Api Client

//JsonApiClient.ts -- this is a different file
import { ApiClient } from "./ApiClient";

export class JsonApiClient implements ApiClient {
  async sendRequest(endpoint: string, data: any): Promise<any> {
    //the fetch API being used to make network requests
    const response = fetch(endpoint, {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify(data),
    });

    return this.parseResponse((await response).json());
  }
  parseResponse(response: any) {
    return response;
  }
}

XML Api Client

// XmlApiClient.ts
import { ApiClient } from "./ApiClient";

export class XmlApiClient implements ApiClient {
  parseResponse(response: any): any {
    const parser = new DOMParser();
    const xmlDoc = parser.parseFromString(response, "application/xml");
    return xmlDoc;
  }

  async sendRequest(endpoint: string, data: any): Promise<any> {
    //XML serialisation
    const xmlData = new XMLSerializer().serializeToString(data);
    //fetch API to make the network request and get the response
    const response = await fetch(endpoint, {
      method: "POST",
      headers: { "Content-Type": "application/xml" },
      body: xmlData,
    });

    const text = await response.text();
    return this.parseResponse(text);
  }
}

YAML Api Client

Before implementing the YAML client code, we need to install the npm package

npm install js-yaml
//YamlApiClient.ts
import { ApiClient } from "./ApiClient";
//import

export class YamlApiClient implements ApiClient {
  async sendRequest(endpoint: string, data: any): Promise<any> {
    const yaml = require("js-yaml");

    //convert typescript object to yaml
    const yamlData = yaml.dump(data);
    const response = await fetch(endpoint, {
      method: "POST",
      headers: { "Content-Type": "application/x-yaml" },
      body: yamlData,
    });

    const text = await response.text();
    return this.parseResponse(text);
  }
  parseResponse(response: any): any {
    const yaml = require("js-yaml");
    return yaml.load(response);
  }
}

Step 3: Define the Factory Method Interface

Define an abstract class that declares the factory method. This method will return the instance of ApiClient. Note, how we have nicely decoupled the creation of the client from the remainder of the business logic.

//ApiClientFactoryMethod.ts
import { ApiClient } from "./ApiClient";

export abstract class ApiClientFactoryMethod {
  //The factory method
  abstract createApiClient(): ApiClient;

  //common method that uses the factory method
  async fetchData(endpoint: string, data: any) {
    const client = this.createApiClient();
    const response = await client.sendRequest(endpoint, data);
  }
}

Step 4: Implement Concrete Creators

Create subclasses that implement the factory method to return the appropriate ApiClient instance.

JSON API Client Factory

// JsonApiClientFactoryMethod.ts

import { ApiClientFactoryMethod } from './ApiClientFactoryMethod';
import { JsonApiClient } from './JsonApiClient';
import { ApiClient } from './ApiClient';

export class JsonApiClientFactoryMethod extends ApiClientFactoryMethod {
    createApiClient(): ApiClient {
        return new JsonApiClient();
    }
}

XML API Client Factory

import { ApiClient } from "./ApiClient";
import { ApiClientFactoryMethod } from "./ApiClientFactoryMethod";
import { XmlApiClient } from "./XmlApiClient";

export class XmlClientFactoryMethod extends ApiClientFactoryMethod {
  createApiClient(): ApiClient {
    return new XmlApiClient();
  }
}

YAML API Client Factory

import { ApiClient } from "./ApiClient";
import { ApiClientFactoryMethod } from "./ApiClientFactoryMethod";
import { YamlApiClient } from "./YamlApiClient";

export class YamlClientFactoryMethod extends ApiClientFactoryMethod {
  createApiClient(): ApiClient {
    return new YamlApiClient();
  }
}

Step 5: Use the Factory Method in Client Code

Use the factory methods to create and use different API clients without worrying about their concrete implementations.

import { JsonApiClientFactoryMethod } from "./JsonApiClientFactoryMethod";
import { XmlClientFactoryMethod } from "./XmlApiClientFactoryMethod";
import { YamlClientFactoryMethod } from "./YamlClientFactoryMethod";

function createXmlData(): Document {
  const parser = new DOMParser();
  const xmlstring = "<request><key>value</key></request>";
  return parser.parseFromString(xmlstring, "application/xml");
}
async function main() {
  //using json api client
  const jsonFactory = new JsonApiClientFactoryMethod();
  await jsonFactory.fetchData("https://api.demo.com/json", {
    key: "value",
    key1: "value1",
  });

  //using xml api client
  const xmlFactory = new XmlClientFactoryMethod();
  await xmlFactory.fetchData("https://api.demo.com/xml", createXmlData());

  //using yaml api client
  const yamlFactory = new YamlClientFactoryMethod();
  await yamlFactory.fetchData("https://api.example.com/yaml", { key: "value" });
}

main();

Explanation of the workflow:

  1. ApiClientFactoryMethod (Abstract Creator)

    This class declares the factory method and provides a common method fetchData that uses the ApiClient.

  2. JsonApiClientFactoryMethod,XmlClientFactoryMethod,YamlClientFactoryMethod are the concrete creators -Each subclass implements the createApiClient() method to return a specific ApiClient instance.

    The client code now depends on the ApiClient interface and the factory method, not on the concrete implementations (JsonApiClient, XmlApiClient, YamlApiClient). This reduces coupling because the client code doesn’t need to know the details of the concrete classes.

  3. mainFactory.ts (Client Code)

    Creates instances of the concrete factory classes.

    Calls fetchData() on each factory, which internally uses the appropriate ApiClient.

Conclusion:

Encapsulation: Object creation is encapsulated within factory methods.

Scalability: Easily add new API clients by creating new factory subclasses without modifying existing code.

Maintainability: Reduces dependencies between client code and concrete classes, making the system easier to maintain.

The GITHUB LINK for the code sample is available.