Abstract Factory

Last updated Apr 16th, 2019

About this...

The abstract factory pattern provides a way to encapsulate a group of individual factories that have a common theme without specifying their concrete classes.




Here's a really silly example. If you don't like it, well- damn. If you do, tell me about it.

Pokemon Example w/ TypeScript

Let's say you want to be able to create any type of Pokemon.

""

I know that's a bold ask especially since there's new Pokemon getting added and created all the time.

While that is true, if we wanted to do this, we'd need to start somewhere.

So let's define the Pokemon abstraction.

Pokemon abstract class

All Pokemon must extend this abstract class

interface IPokemonProps {
  name: string;
  color: string;
}

abstract class Pokemon implements IPokemonProps {
  public name: string;
  public color: string;

  constructor (props: IPokemonProps) {
    this.name = props.name;
    this.color = props.color;
  }

  abstract attack (): IAttack;
}

Every pokemon must have a name and a color and the concrete pokemon class must implement the abstract attack method. Different pokemon have different attacks, right? That's why it's abstract. The subclass will define it.

OK, let's create our first Pokemon. So let's start with Pikachu.

How do we create a Pikachu?

Our first Factory, a Pikachu Factory

I'm not sure if it was ever really discussed in the show how Pokemon are actually created...

Hypothetically, let's say that if we wanted to create a Pikachu, we'd need to do the following:

  • get a bunch of batteries
  • get some tape
  • get some paint
  • get a cat
  • tape the batteries to the back of a cat

Use your imagination here, OK?

Since there's obviously a process involved in creating this particular Pokemon, let's put this into an abstraction, a factory.

class Pikachu extends Pokemon {
  private cat: TapedItem<Battery[], Cat>;

  constructor (cat: TapedItem<Battery[], Cat>) {
    super({ name: 'Pikachu', color: 'yellow' });
    this.cat = cat;
  }

  attack () : ZapAttack {
    return this.cat.zapAttack();
  }
}

class PikachuFactory {
  public static create (): Pikachu {
    const batteries: Battery[] = [
      new Battery(),
      new Battery()
    ];
    const paint: Paint = new Paint('yellow');
    const tape: Tape = new Tape();
    const cat: Cat = new Cat();

    const paintedCat: PaintedItem<Cat> = Paint.paintItem(cat, paint);

    const catTapedByBatteries: TapedItem<Battery[], Cat> = Tape
      .combineItems(batteries, paintedCat);

    return new Pikachu(catTapedByBatteries);
  }
}

OK awesome, we have a way to create Pikachus.

We can do that like this.

const pikachu: Pikachu = PikachuFactory.create();

And we've encapsulated all of the complex Pikachu-creation logic inside of a Factory.

Woo!

More Pokemon Factories

Now let's say that we wanted to create a Charmander factory, a Bulbasaur factory and a Porygon factory. And each of them would also have equally creative and complex ways to create them, encapsulated inside of some type of Pokemon Factory.

And we wanted to be able to create them all of them like this:

const charmander: Charmander = CharmanderFactory.create();
const bulbasaur: Bulbasaur = BulbasaurFactory.create();
const porygon: Porygon = PorygonFactory.create();

And more!

As much fun as it would be to create more Pokemon Factories, I'm going to have to assume you get the idea.


At this point, I will finally be able to present to you the usefulness of an Abstract Factory.

Ideally, we would want to encapsulate Pokemon creation somehow.

By definition:

The abstract factory pattern provides a way to encapsulate a group of individual factories that have a common theme without specifying their concrete classes.

That definition probably makes a little bit more sense now given the context.

Instead of needing to create a hard source code dependency by importing the type of Pokemon that we want like this:

import { Charmander } from 'pokemon/charmander'
import { CharmanderFactory } from 'pokemon/charmander/factory'
import { Bulbasaur } from 'pokemon/bulbasaur'
import { BulbasaurFactory } from 'pokemon/bulbasaur/factory'
import { Porygon } from 'pokemon/porygon'
import { PorygonFactory } from 'pokemon/porygon/factory'

const charmander: Charmander = CharmanderFactory.create();
const bulbasaur: Bulbasaur   = BulbasaurFactory.create();
const porygon: Porygon       = PorygonFactory.create();

We can call upon an abstract PokemonFactory like this:

import { PokemonFactory, PokemonType } from 'pokemon/factory'
import { Charmander } from 'pokemon/charmander'
import { Bulbasaur } from 'pokemon/bulbasaur'
import { Porygon } from 'pokemon/porygon'

const charmander: Charmander = PokemonFactory.create(PokemonType.CHARMANDER);
const bulbasaur: Bulbasaur   = PokemonFactory.create(PokemonType.BULBASAUR);
const porygon: Porygon       = PokemonFactory.create(PokemonType.PORYGON);

Where the PokemonFactory looks like:

import { CharmanderFactory } from 'pokemon/charmander/factory'
import { BulbasaurFactory } from 'pokemon/bulbasaur/factory'
import { PorygonFactory } from 'pokemon/porygon/factory'

enum PokemonType {
  CHARMANDER = 'charmander',
  BULBASAUR = 'bulbasaur',
  PORYGON = 'porygon'
}

export class PokemonFactory {
  public static create(pokemonType: PokemonType): Pokemon {
    switch (pokemonType) {
      case PokemonType.CHARMANDER:
        return CharmanderFactory.create();
      case PokemonType.BULBASAUR:
        return BulbasaurFactory.create();
      case PokemonType.PORYGON:
        return PorygonFactory.create();
      default:
        return null;
    }
  }
}

Why is it useful?

What we've done here is abstracted how we create Pokemon.

We've also delegated the Single Responsibility for Pokemon creation to one place.

When we have to add new Pokemon, we add a new PokemonType, create the new factory and add it to the end of this switch statement.

3 Comments

Commenting has been disabled for now. To ask questions and discuss this post, join the community.

Julien Heller
4 years ago

Isn't this violating the Open/Close principle, since you need to modify the `PokemonFactory` when extending?

Khalil Stemmler
4 years ago

Great question.


This is as Open-Closed as theoretically possible.


It's Open-Closed at the class and relationship level. Extensions to it never need to modify the existing classes (all those other pokemon factories).


The abstraction is closed. A PokemonFactory will always return a Pokemon.


But here's the rub: in order to add new options to a selection, we'll need to add a little bit of code.


It's impossible for a program to know about something we've written unless we hook it up somehow.


New code will always need to be added, but OCP aims to minimize the amount of modifications that affect existing code, to 0.


We could have pulled the switch statement out and put it somewhere else, but the fact is that we'll always need to add at least one more line of code.

Julien Heller
4 years ago

For those interested, this post inspired me to create a small tool for implementing this pattern via decorators:

https://github.com/flux627/abstract-factory-factory

Katyusha
a year ago

Let's assume that we want go back and use your PikachuFactory to build a Pikachu with your final Pokemon Factory:

class Pikachu extends Pokemon {
  private cat: TapedItem<Battery[], Cat>;

  constructor (cat: TapedItem<Battery[], Cat>) {
    super({ name: 'Pikachu', color: 'yellow' });
    this.cat = cat;
  }

  attack () : ZapAttack {
    return this.cat.zapAttack();
  }
}

class PikachuFactory {
  public static create (): Pikachu {
    const batteries: Battery[] = [
      new Battery(),
      new Battery()
    ];
    const paint: Paint = new Paint('yellow');
    const tape: Tape = new Tape();
    const cat: Cat = new Cat();

    const paintedCat: PaintedItem<Cat> = Paint.paintItem(cat, paint);

    const catTapedByBatteries: TapedItem<Battery[], Cat> = Tape
      .combineItems(batteries, paintedCat);

    return new Pikachu(catTapedByBatteries);
  }
}


And we add PIKACHU to the list of enums:

enum PokemonType {
  CHARMANDER = 'charmander',
  BULBASAUR = 'bulbasaur',
  PORYGON = 'porygon',
  PIKACHU = 'pickachu'  
}

And then, we add the PikachuFactory to the PokemonFactory switch statement:

switch (pokemonType) {
      case PokemonType.CHARMANDER:
        return CharmanderFactory.create();
      case PokemonType.BULBASAUR:
        return BulbasaurFactory.create();
      case PokemonType.PORYGON:
        return PorygonFactory.create();
      case PokemonType.PIKACHU:
        return PikachuFactory.create();
      default:
        return null;
}

When we call the factory:

import { PokemonFactory, PokemonType } from 'pokemon/factory'
import { Charmander } from 'pokemon/charmander'
import { Bulbasaur } from 'pokemon/bulbasaur'
import { Porygon } from 'pokemon/porygon'

const charmander: Charmander = PokemonFactory.create(PokemonType.CHARMANDER);
const bulbasaur: Bulbasaur   = PokemonFactory.create(PokemonType.BULBASAUR);
const porygon: Porygon       = PokemonFactory.create(PokemonType.PORYGON);
const pikachu: Pikachu       = PokemonFactory.create(PokemonType.PIKACHU);

Wouldn't the compiler fail since the cat property is missing in type Pokemon but is required in type Pikachu?