Abstract Factory Pattern

The Abstract Factory Pattern is another creational design pattern that provides an interface for creating families of related or dependent objects without specifying their concrete classes. Instead of just creating one type of object, like in the Factory Method pattern, the Abstract Factory pattern lets you create related objects that can work together.

Let's build on the ApiClientFactoryMethod example we discussed in the blog on Factory Design pattern and transition into understanding the Abstract Factory Pattern.

Difference from Factory Method

  • Factory Method: Deals with the creation of a single object type.

  • Abstract Factory: Deals with the creation of families of related objects, ensuring that objects created by the factory are compatible with each other.

Step 1: Define interfaces for each family of objects

Let us define the interfaces for logger, parser and the api client.

// ApiClient.ts
export interface ApiClient {
    fetchData(endpoint: string): Promise<any>;
}

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

// ApiParser.ts
export interface ApiParser {
    parse(data: string): any;
}

Step 2: Create concrete implementations for each API type

//XmlApiClient.ts

import { ApiClient } from "./ApiClient";
import { ApiLogger } from "./ApiLogger";
import { ApiParser } from "./ApiParser";

export class XmlApiClient implements ApiClient {
  fetchData(endpoint: string): Promise<any> {
    console.log(`Fetching data from XML API: ${endpoint}`);
    return Promise.resolve('{ "data": "XML data" }');
  }
}

export class XmlApiLogger implements ApiLogger {
  log(message: string): void {
    console.log(`[XML Logger]: ${message}`);
  }
}

export class XmlApiParser implements ApiParser {
  parse(data: string): any {
    // Mock XML parsing
    return { data: "Parsed XML data" };
  }
}
//YamlApiClient.ts

import { ApiClient } from "./ApiClient";
import { ApiLogger } from "./ApiLogger";
import { ApiParser } from "./ApiParser";

export class YamlApiClient implements ApiClient {
  fetchData(endpoint: string): Promise<any> {
    console.log(`Fetching data from YAML API: ${endpoint}`);
    return Promise.resolve('{ "data": "YAML data" }');
  }
}

export class YamlApiLogger implements ApiLogger {
  log(message: string): void {
    console.log(`[YAML Logger]: ${message}`);
  }
}

export class YamlApiParser implements ApiParser {
  parse(data: string): any {
    // Mock YAML parsing
    return { data: "Parsed YAML data" };
  }
}

//JsonApiClient.ts

import { ApiClient } from "./ApiClient";
import { ApiLogger } from "./ApiLogger";
import { ApiParser } from "./ApiParser";

export class JsonApiClient implements ApiClient {
  fetchData(endpoint: string): Promise<any> {
    console.log(`Fetching data from JSON API: ${endpoint}`);
    return Promise.resolve('{ "data": "JSON data" }');
  }
}

export class JsonApiLogger implements ApiLogger {
  log(message: string): void {
    console.log(`[JSON Logger]: ${message}`);
  }
}

export class JsonApiParser implements ApiParser {
  parse(data: string): any {
    console.log(`[JSON parser]: ${data}`);
    return JSON.parse(data);
  }
}

Step 3: Create the Abstract Factory interface

Define an abstract factory interface that groups these related components together.

//ApiFactory.ts
import { ApiClient } from "./ApiClient";
import { ApiLogger } from "./ApiLogger";
import { ApiParser } from "./ApiParser";

export interface ApiFactory {
  createClient(): ApiClient;
  createLogger(): ApiLogger;
  createParser(): ApiParser;
}

Step 4: Implement Concrete Factories for each API type

Create the concrete factories that will produce appropriate objects for each API type.

//JsonApiFactory.ts

import { ApiClient } from "./ApiClient";
import { ApiFactory } from "./ApiFactory";
import { ApiLogger } from "./ApiLogger";
import { ApiParser } from "./ApiParser";
import { JsonApiClient, JsonApiLogger, JsonApiParser } from "./JsonAPiClient";

export class JsonApiFactory implements ApiFactory {
  createClient(): ApiClient {
    return new JsonApiClient();
  }
  createLogger(): ApiLogger {
    return new JsonApiLogger();
  }
  createParser(): ApiParser {
    return new JsonApiParser();
  }
}

//YamlApiFactory.ts
import { ApiClient } from "./ApiClient";
import { ApiFactory } from "./ApiFactory";
import { ApiLogger } from "./ApiLogger";
import { ApiParser } from "./ApiParser";
import { YamlApiClient, YamlApiLogger, YamlApiParser } from "./YamlApiClient";

export class YamlApiFactory implements ApiFactory {
  createClient(): ApiClient {
    return new YamlApiClient();
  }
  createLogger(): ApiLogger {
    return new YamlApiLogger();
  }
  createParser(): ApiParser {
    return new YamlApiParser();
  }
}
//XmlApiFactory.ts

import { ApiClient } from "./ApiClient";
import { ApiFactory } from "./ApiFactory";
import { ApiLogger } from "./ApiLogger";
import { ApiParser } from "./ApiParser";
import { XmlApiClient, XmlApiLogger, XmlApiParser } from "./XmlApiClient";

export class XmlApiFactory implements ApiFactory {
  createClient(): ApiClient {
    return new XmlApiClient();
  }
  createLogger(): ApiLogger {
    return new XmlApiLogger();
  }
  createParser(): ApiParser {
    return new XmlApiParser();
  }
}

Step 5: Creating A Registry that will register the factories and also newer factories in the future.

import { ApiFactory } from "./ApiFactory";

class ApiClientFactoryRegistry {
  private static registry: { [key: string]: ApiFactory } = {};

  static registerFactory(type: string, factory: ApiFactory): void {
    this.registry[type] = factory;
  }

  static getFactory(type: string): ApiFactory {
    const factory = this.registry[type];
    if (!factory) {
      throw new Error(`No factory registered for type : {$type}`);
    }
    return factory;
  }
}

The registry class has a method registerFactory which will store the mapping between the API type and its corresponding factory.

Step 6: Working code

//main.ts

//registering the three factories
import { ApiClientFactoryRegistry } from "./ApiClientFactoryRegistry";
import { JsonApiFactory } from "./JsonApiFactory";
import { XmlApiFactory } from "./XmlApiFactory";
import { YamlApiFactory } from "./YamlApiFactory";

function main() {
  //register the three factories
  ApiClientFactoryRegistry.registerFactory("json", new JsonApiFactory());
  ApiClientFactoryRegistry.registerFactory("yaml", new YamlApiFactory());
  ApiClientFactoryRegistry.registerFactory("xml", new XmlApiFactory());

  //user Input or configuration input
  const apiType = "json";

  // Retrieving a factory
  const apiFactory = ApiClientFactoryRegistry.getFactory(apiType);

  const client = apiFactory.createClient();
  const logger = apiFactory.createLogger();
  const parser = apiFactory.createParser();

  client.fetchData("your-https-endpoint").then((data) => {
    logger.log("data fetched successfully");
    const parsedData = parser.parse(data);
    logger.log(parsedData);
  });
}

main();

Conclusion:

By using the Abstract Factory pattern, you ensure that all related objects (API client, logger, parser) are created together and are compatible with each other, which can be particularly useful in complex systems where we have multiple interrelated components.

The GITHUB LINK code sample for your reference.