Skip to main content
a z C o d e l a n d

Abstract Factory Design Pattern

Created: July 27th, 2024

Updated: July 30th, 2024

General Defintion

Provide an interface for creating families of related or dependent objects without specifying their concrete classes.

— Gang of Four, Design Patterns: Elements of Reusable Object-Oriented Software


Wikipedia article says:

The abstract factory pattern in software engineering is a design pattern that provides a way to create families of related objects without imposing their concrete classes, by encapsulating a group of individual factories that have a common theme without specifying their concrete classes


Explanation

The Abstract Factory Design Pattern is a creational design pattern that provides an abstract interface to be implemented by classes and collectively, these class implementations become known as concrete factory class variants. The implemented methods, collectively, of each variant create a set of related or dependent objects, that are known as a family of objects, which belong to the specific concrete factory class variant they were created from. The Abstract Factory Design Pattern is also referred to as a “Factory of Factories” or a “Super-Factory” because it provides an interface that other classes implement to become factories.


Structure

  • Abstract Factory Interface: The interface that defines the methods that the concrete classes must implement.

  • Concrete Factory Class Variants: The concrete factory class variants that implement the methods of the abstract factory interface. These methods will be used by the client code to instantiate instances of the Concrete Product class variants.

  • Abstract Product Interface: The interface that defines the methods that the concrete product classes must implement.

  • Concrete Product Class Variants: The concrete product class variants that implement the methods of the abstract product interface. These methods are used by the client code to interact with the concrete product class instances.

  • Factory Configuration/Selection: This part of the code determines which concrete factory variant to use based on certain conditions or application settings. It provides the selected concrete factory variant to the client code.

  • Client Code: The code that uses the methods on the abstract factory interface to create instances of the concrete factory class variants. It also uses the methods of the abstract product interface to interact with the concrete product class instances.


Advantages:

  • The client code doesn’t need to specificy which concrete factory class variant to use. The client code is decoupled from the concrete factories. The specific factory to use can be determined at runtime by configuration or environmental factors.

  • The client code doesn’t need to know or care about which concrete factory class variant exists or is being used. The client code interacts with the factories using the abstract factory interface, and since all factories implement it, it’s the only interface that the client code needs to know.

  • The client code can interact with all variants of the abstract factory interface without having to change. All concrete factories implement the same interface and therefore, the client code can interact with all of them without having to change.

  • The client code is consistently receives the right family of objects belonging to a specific concrete factory class variant. This is because each concrete factory produces a family of related or dependent objects

  • Adding or removing a concrete factory class variant requires no change to the client code because the client code is decoupled from the concrete factories.

  • The Factory Configuration/Selection can easily substitute one concrete factory class variant with another with very little change. All concrete factories implement the same interface and therefore, the factory configuration/selection can switch between them.


Disadvantages:

  • Complexity: This approach adds complexity to a codebase because it involves creating multiple classes to manage the creation of a family of related objects.

Analogy

Abstract Factory Interface:

We can think of actions such as draftEmail, sendEmail, deleteEmail, and more, as methods on an interface that we’ll call the EmailServiceProviderInterface, which all email service providers such as Gmail, ICloud Mail, Outlook,Yahoo Mail, etc. have to implement.

Concrete Factory Classes:

The email service providers that implement the EmailServiceProviderInterface interface become the concrete factory class variants, and the methods on our concrete factory class implementations create a set of related or dependent objects, known as a family of objects. In our analogy, the family of objects are the different types of emails, such as DraftedEmail, SentEmail, DeletedEmail, etc.

Abstract Product Interface:

Properties such as date, from, etc., and methods like open, close, reply, forward, moveToFolder, etc., will be defined on the Abstract Product Interface. All the classes that implement this interface become the concrete product classes. Collectively, these form the family of objects. In our analogy, these objects are the instances of the different types of emails, such as DraftEmail, SentEmail, DeletedEmail, etc.

Concrete Product Classes:

The different types of emails, such as DraftEmail, SentEmail, DeletedEmail that implement the Abstract Product Interface become the concrete product classes, which, as we know, represent the related or dependent objects, known as a family of objects.

Client Code:

Let’s consider an Email Viewer application as our Client Code. Some Email Viewer applications allow you to view emails from various email providers all in one place. They allow you to access and manage emails from various email providers such as Gmail, ICloud Mail, Outlook, Yahoo Mail, etc. within the same graphical user interface.

The Email Viewer appication can interact with any email provider without knowing or caring about their specific concrete factory implementations, because all of the email providers implement the same interface. This is advantages for the reasons listed above.

Code Example

For people who have worked with variants of buttons such as primary, secondary, tertiary, etc., the following code example will be familiar. In this code example, the button themes are based on a holiday, such as Easter, Halloween, or Christmas, which serve as our concrete factory class variants from the Explanation section. When the holiday variable is assigned to one of these holidays, the button variants, filled, outline, and disabled will be rendered in the styles for that theme, and these button variants represent the family of objects for each holiday concrete factory class variant.

The code example follows the same implementation structure mentioned earlier. I’ve also included the HTML and CSS, incase you would like to try it out or you can view a live version on Codepen.


  <!DOCTYPE html>
  <html lang="en">
    <head>
      <meta charset="UTF-8" />
      <meta name="viewport" content="width=device-width, initial-scale=1.0" />
      <title>Abstract Factory Pattern</title>
      <link rel="stylesheet" href="style.css" />
    </head>
    <body>
      <script src="main.js"></script>
    </body>
  </html>

  body {
    background-color: #1e1e1e;
    display: grid;
    grid-template-columns: 1fr 1fr 1fr;
    place-items: center;
    block-size: 100vh;
    margin: 0;
  }

  .btn {
    padding: 8px 16px;
    border-radius: 4px;
    border: none;
    cursor: pointer;
    inline-size: 110px;
  }


  .btn-easter-filled {
    color: white;
    background-color: mediumorchid;
  }
  .btn-easter-outlined {
    background-color: transparent;
    border: 1px solid mediumorchid;
    color: mediumorchid;
    font-weight: bold;
  }


  .btn-halloween-filled {
    background-color: darkorange;
  }
  .btn-halloween-outlined {
    background-color: transparent;
    border: 1px solid darkorange;
    color: darkorange;
    font-weight: bold;
  }


  .btn-christmas-filled {
    background-color: darkgreen;
    color: white;
  }
  .btn-christmas-outlined {
    background-color: transparent;
    border: 1px solid #008000;
    color: #008000;
    font-weight: bold;
  }


  .btn-easter-disabled,
  .btn-halloween-disabled,
  .btn-christmas-disabled {
    background-color: darkgray;
    color: #686868;
    cursor: unset;
  }

Abstract Factory:

ES6 Syntax

  class ButtonFactoryInterface {

    constructor() {
      if (new.target === ButtonFactoryInterface) {
        throw new TypeError("Cannot instantiate ButtonFactoryInterface");
      }
    }

    createFilledButton() {
      throw new Error('This method should be overridden');
    }
    createOutlinedButton() {
      throw new Error('This method should be overridden');
    }
    createDisabledButton() {
      throw new Error('This method should be overridden');
    }
  }

Concrete Factories:

ES6 Syntax

  class EasterButtonFactory extends ButtonFactoryInterface {
    createFilledButton() {
      return new EasterFilledButton();
    }
    createOutlinedButton() {
      return new EasterOutlinedButton();
    }
    createDisabledButton() {
      return new EasterDisabledButton();
    }
  }

  class HalloweenButtonFactory extends ButtonFactoryInterface {
    createFilledButton() {
      return new HalloweenFilledButton();
    }
    createOutlinedButton() {
      return new HalloweenOutlinedButton();
    }
    createDisabledButton() {
      return new HalloweenDisabledButton();
    }
  }

  class ChristmasButtonFactory extends ButtonFactoryInterface {
    createFilledButton() {
      return new ChristmasFilledButton();
    }
    createOutlinedButton() {
      return new ChristmasOutlinedButton();
    }
    createDisabledButton() {
      return new ChristmasDisabledButton();
    }
  }

Abstract Product:

ES6 Syntax

  class ButtonProductInterface {
    button = null;
    constructor() {
      if (new.target === ButtonProductInterface) {
        throw new TypeError("Cannot instantiate ButtonProductInterface");
      }
    }
    render() {
      throw new Error('This method should be overridden');
    }
    onClick(callback) {
      throw new Error('This method should be overridden');
    }
  }

Concrete Products:

ES6 Syntax

  // This is a base class and not a concrete product
  class BaseButton extends ButtonProductInterface {
    constructor() {
      super();
      this.button = document.createElement('button');
      this.button.classList.add('btn');
      // instead of adding styles through js, I've used css
      // this.button.padding = '8px 16px';
      // this.button.borderRadius = '4px';
      // this.button.border = 'none';
      // this.button.cursor = 'pointer';
    }
    render() {
      document.body.appendChild(this.button);
    }
    onClick(callback) {
      this.button.addEventListener('click', callback);
    }
  }

  // This is a base class and not a concrete product
  class BaseEasterButton extends BaseButton {
    constructor() {
      super();
      this.button.innerText = 'Easter';
    }
  }

  class EasterFilledButton extends BaseEasterButton {
    constructor() {
      super();
      this.button.classList.add('btn-easter-filled');
    }
  }

  class EasterOutlinedButton extends BaseEasterButton {
    constructor() {
      super();
      this.button.classList.add('btn-easter-outlined');
    }
  }

  class EasterDisabledButton extends BaseEasterButton {
    constructor() {
      super();
      this.button.classList.add('btn-easter-disabled');
    }
  }

  // This is a base class and not a concrete product
  class BaseHalloweenButton extends BaseButton {
    constructor() {
      super();
      this.button.innerText = 'Halloween';
    }
  }

  class HalloweenFilledButton extends BaseHalloweenButton {
    constructor() {
      super();
      this.button.classList.add('btn-halloween-filled');
    }
  }

  class HalloweenOutlinedButton extends BaseHalloweenButton {
    constructor() {
      super();
      this.button.classList.add('btn-halloween-outlined');
    }
  }

  class HalloweenDisabledButton extends BaseHalloweenButton {
    constructor() {
      super();
      this.button.classList.add('btn-halloween-disabled');
    }
  }


  // This is a base class and not a concrete product
  class BaseChristmasButton extends BaseButton {
    constructor() {
      super();
      this.button.innerText = 'Christmas';
    }
  }

  class ChristmasFilledButton extends BaseChristmasButton {
    constructor() {
      super();
      this.button.classList.add('btn-christmas-filled');
    }
  }

  class ChristmasOutlinedButton extends BaseChristmasButton {
    constructor() {
      super();
      this.button.classList.add('btn-christmas-outlined');
    }
  }

  class ChristmasDisabledButton extends BaseChristmasButton {
    constructor() {
      super();
      this.button.classList.add('btn-christmas-disabled');
    }
  }

By having the classes BaseButton, BaseEasterButton, BaseHalloweenButton, and BaseChristmasButton, our abstract factory design pattern implementation slightly deviates from the standard definition of the abstract factory pattern. This is okay because we’re supposed to customize design patterns to fit our needs rather than making our implementation fit the standard definition.


Factory Selection:

The selection of the specific factory is usually handled outside of the client code, and it can be based on various factors such as the operating system, application setup, configuration, etc. This steup provides the appropriate concrete factory variant to the client. Even if the client code was responsible for selecting the factory variant, the code change required would be minimal to select a different factory variant.

In this code example, the holidy variable could be assigned programmatically based on actual holiday dates, but for simplicity, it’s just assigned the holidy as a string.


ES6 Syntax

  const holidayThemeButtons = {
    'Easter': new EasterButtonFactory,
    'Halloween': new HalloweenButtonFactory,
    'Christmas': new ChristmasButtonFactory,
  };

  const holiday = 'Halloween';
  const holidayThemeButtonFactory = holidayThemeButtons[holiday];

Client Code:

ES6 Syntax

  let filledButton = holidayThemeButtonFactory.createFilledButton();
  let outlinedButton = holidayThemeButtonFactory.createOutlinedButton();
  let disabledButton = holidayThemeButtonFactory.createDisabledButton();

  filledButton.render();
  outlinedButton.render();
  disabledButton.render();

Easter Theme Output:

Disadvantage #2

Halloween Theme Output:

Disadvantage #2

Christmas Theme Output:

Disadvantage #2

Another Code Example

I’ve been trying to be mindful of practical use cases while learning design patterns, which has helped because I noticed a problem that I am currently facing: I would like to make blogposts available in JavaScript, TypeScript, and Python, is well suited for the abstract factory design pattern.


Potenital Solutions, Pros and Cons:

  1. Use different language codeblocks and make the whole blogpost generic so the contents apply to all languages.

    • Cons:
      • Lose important pattern implementation details specific to each language.
      • Lose language-specific syntax and concepts in the explanations.
    • Pro: Easier than the next solution.

  2. Create different versions of each blogpost and give the viewer the option to view it in their choice of language.

    • Con: Requires more time to create two versions of each blogpost.
    • Pro:
      • Retain important details specific to the implementations of the patterns in each language.
      • Retain language-specific syntax and concepts in the explanations.

I’ve decided to go with #2 because the language-specific details matter between Python and the other two languages, JS and TS. However, between JavaScript and TypeScript, I only need to change the codeblocks to show the different syntaxes, and so I can reuse the content of the blogpost. #2 is also beneficial because, by writing blogposts specific to each language, I will learn more about the pattern implementation details in those languages.


Implementation:

ES6 Syntax

  // Abstract Factory
  class BlogPostAbstractFactory {
    constructor(){
      if (new.target === BlogPostAbstractFactory){
        throw new TypeError("Cannot instantiate BlogPostAbstractFactory");
      }
    }

    createJavaScriptBlogPost(){
      throw new Error('This method should be overridden');
    }      
    createTypeScriptBlogPost(){
      throw new Error('This method should be overridden');
    }      
    createPythonBlogPost(){
      throw new Error('This method should be overridden');
    }      
  }

  // Concrete Factory
  class BlogPostFactory extends BlogPostAbstractFactory {
    createJavaScriptBlogPost(){
      return new JavaScriptBlogPost();
    }
    createTypeScriptBlogPost(){
      return new TypeScriptBlogPost();
    }
    createPythonBlogPost(){
      return new PythonBlogPost();
    }
  }

  // Abstract Product
  class BlogPostAbstract {
    constructor() {
      if (new.target === BlogPostAbstract) {
        throw new TypeError("Cannot instantiate BlogPostAbstract");
      }
    }

    getBlogPost(fileName) {
      throw new Error('This method should be overridden');
    }

    publishBlogPost() {
      throw new Error('This method should be overridden');
    }
  }

  // Concrete Products
  class JavaScriptBlogPost extends BlogPostAbstract {
    getBlogPost(fileName) {
      const file = `filePath\${fileName}`
      // Retrieve the JavaScript blogpost content
    }

    publishBlogPost() {
      // Publish the JavaScript blogpost
    }
  }

  // ...code for TypeScriptBlogPost and PythonBlogPost

Factory Selection:

  const blogPostFactory = new BlogPostFactory();
  const blogPost = blogPostFactory.createJavaScriptBlogPost();

Client Code:

  blogPost.getBlogPost(fileName);
  blogPost.publishBlogPost();

Abstract Factory Pattern Before ES6

Prior to ES6, the class syntax that we used above did not exist, so functions and prototypes were used instead. As can example, the below code re-implements the BlogPost Abstract Factory classes, but using ES5 syntax.


Implementation:

ES5 Syntax

  // Abstract Factory
  function BlogPostAbstractFactory() {
    if (this.constructor === BlogPostAbstractFactory) {
      throw new TypeError("Cannot instantiate BlogPostAbstractFactory");
    }
  }

  BlogPostAbstractFactory.prototype.createJavaScriptBlogPost = function() {
    throw new Error('This method should be overridden');
  };

  BlogPostAbstractFactory.prototype.createTypeScriptBlogPost = function() {
    throw new Error('This method should be overridden');
  };

  BlogPostAbstractFactory.prototype.createPythonBlogPost = function() {
    throw new Error('This method should be overridden');
  };

  // Concrete Factory
  function BlogPostFactory() {
    BlogPostAbstractFactory.call(this);
  }

  BlogPostFactory.prototype = Object.create(BlogPostAbstractFactory.prototype);

  BlogPostFactory.prototype.createJavaScriptBlogPost = function() {
    return new JavaScriptBlogPost();
  };

  BlogPostFactory.prototype.createTypeScriptBlogPost = function() {
    return new TypeScriptBlogPost();
  };

  BlogPostFactory.prototype.createPythonBlogPost = function() {
    return new PythonBlogPost();
  };

  // Abstract Product
  function BlogPostAbstract() {
    if (this.constructor === BlogPostAbstract) {
      throw new TypeError("Cannot instantiate BlogPostAbstract");
    }
  }

  BlogPostAbstract.prototype.getBlogPost = function(fileName) {
    throw new Error('This method should be overridden');
  };

  BlogPostAbstract.prototype.publishBlogPost = function() {
    throw new Error('This method should be overridden');
  };

  // Concrete Products
  function JavaScriptBlogPost() {
    BlogPostAbstract.call(this);
  }

  JavaScriptBlogPost.prototype = Object.create(BlogPostAbstract.prototype);

  JavaScriptBlogPost.prototype.getBlogPost = function(fileName) {
    var file = 'filePath' + fileName;
    // Retrieve the JavaScript blogpost content
  };

  JavaScriptBlogPost.prototype.publishBlogPost = function() {
    // Publish the JavaScript blogpost
  };

  // ...code for TypeScriptBlogPost and PythonBlogPost

Factory Selection:

  var blogPostFactory = new BlogPostFactory();
  var blogPost = blogPostFactory.createJavaScriptBlogPost();

Client Code:

  blogPost.getBlogPost(fileName);
  blogPost.publishBlogPost();

Wrapping Up

Learning about the factory design pattern has been a little confusing for me, and I think it’s because of two reasons.

  1. While searching online, I’ve noticed there are many implementations of the pattern. Even though I understand the various implementations are because design patterns are general, reusable solutions to commonly occurring problems in software design, nonetheless, these variations added to my confusion.

  2. I realize there is confusion around the difference between the factory design pattern, the factory method pattern, and the abstract factory design pattern.


I’m happy to understand another design pattern! I hope this blogpost was helpful, thank you for reading!

Resources


© 2024 NazCodeland