Skip to content

Abstract Factory Design Pattern

Video Lecture

Section Video Links
Abstract Factory Overview Abstract Factory Overview Abstract Factory Overview 
Abstract Factory Use Case Abstract Factory Use Case Abstract Factory Use Case 
Exception Handling Exception Handling Exception Handling 

Overview

The Abstract Factory Pattern adds an abstraction layer over multiple other creational pattern implementations.

To begin with, in simple terms, think if it as a Factory that can return Factories. Although you will find examples of it also being used to return Builder, Prototypes, Singletons or other design pattern implementations.

Terminology

  • Client: The client application that calls the Abstract Factory. It's the same process as the Concrete Creator in the Factory design pattern.

  • Abstract Factory: A common interface over all the sub factories.

  • Concrete Factory: The sub factory of the Abstract Factory and contains method(s) to allow creating the Concrete Product.

  • Abstract Product: The interface for the product that the sub factory returns.

  • Concrete Product: The object that is finally returned.

Abstract Factory UML Diagram

Abstract Factory Overview

Source Code

./abstract_factory/abstract_factory_concept.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
# pylint: disable=too-few-public-methods
"Abstract Factory Concept Sample Code"
from abc import ABCMeta, abstractmethod
from factory_a import FactoryA
from factory_b import FactoryB

class IAbstractFactory(metaclass=ABCMeta):
    "Abstract Factory Interface"

    @staticmethod
    @abstractmethod
    def create_object(factory):
        "The static Abstract factory interface method"

class AbstractFactory(IAbstractFactory):
    "The Abstract Factory Concrete Class"

    @staticmethod
    def create_object(factory):
        "Static get_factory method"
        try:
            if factory in ['aa', 'ab', 'ac']:
                return FactoryA.create_object(factory[1])
            if factory in ['ba', 'bb', 'bc']:
                return FactoryB.create_object(factory[1])
            raise Exception('No Factory Found')
        except Exception as _e:
            print(_e)
        return None

# The Client
PRODUCT = AbstractFactory.create_object('ab')
print(f"{PRODUCT.__class__}")

PRODUCT = AbstractFactory.create_object('bc')
print(f"{PRODUCT.__class__}")

./abstract_factory/factory_a.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
# pylint: disable=too-few-public-methods
"FactoryA Sample Code"
from abc import ABCMeta, abstractmethod

class IProduct(metaclass=ABCMeta):
    "A Hypothetical Class Interface (Product)"

    @staticmethod
    @abstractmethod
    def create_object():
        "An abstract interface method"

class ConcreteProductA(IProduct):
    "A Concrete Class that implements the IProduct interface"

    def __init__(self):
        self.name = "ConcreteProductA"

    def create_object(self):
        return self

class ConcreteProductB(IProduct):
    "A Concrete Class that implements the IProduct interface"

    def __init__(self):
        self.name = "ConcreteProductB"

    def create_object(self):
        return self

class ConcreteProductC(IProduct):
    "A Concrete Class that implements the IProduct interface"

    def __init__(self):
        self.name = "ConcreteProductC"

    def create_object(self):
        return self

class FactoryA:
    "The FactoryA Class"

    @staticmethod
    def create_object(some_property):
        "A static method to get a concrete product"
        try:
            if some_property == 'a':
                return ConcreteProductA()
            if some_property == 'b':
                return ConcreteProductB()
            if some_property == 'c':
                return ConcreteProductC()
            raise Exception('Class Not Found')
        except Exception as _e:
            print(_e)
        return None

./abstract_factory/factory_b.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
# pylint: disable=too-few-public-methods
"FactoryB Sample Code"
from abc import ABCMeta, abstractmethod

class IProduct(metaclass=ABCMeta):
    "A Hypothetical Class Interface (Product)"

    @staticmethod
    @abstractmethod
    def create_object():
        "An abstract interface method"

class ConcreteProductA(IProduct):
    "A Concrete Class that implements the IProduct interface"

    def __init__(self):
        self.name = "ConcreteProductA"

    def create_object(self):
        return self

class ConcreteProductB(IProduct):
    "A Concrete Class that implements the IProduct interface"

    def __init__(self):
        self.name = "ConcreteProductB"

    def create_object(self):
        return self

class ConcreteProductC(IProduct):
    "A Concrete Class that implements the IProduct interface"

    def __init__(self):
        self.name = "ConcreteProductC"

    def create_object(self):
        return self

class FactoryB:
    "The FactoryB Class"

    @staticmethod
    def create_object(some_property):
        "A static method to get a concrete product"
        try:
            if some_property == 'a':
                return ConcreteProductA()
            if some_property == 'b':
                return ConcreteProductB()
            if some_property == 'c':
                return ConcreteProductC()
            raise Exception('Class Not Found')
        except Exception as _e:
            print(_e)
        return None

Output

python ./abstract_factory/abstract_factory_concept.py
<class 'factory_a.ConcreteProductB'>
<class 'factory_b.ConcreteProductC'>

SBCODE Editor

<>

Abstract Factory Example Use Case

An example use case may be that you have a furniture shopfront. You sell many kinds of furniture. You sell chairs and tables. And they are manufactured at different factories using different unrelated processes that are not important for your concern. You only need the factory to deliver.

You can create an extra module called FurnitureFactory, to handle the chair and table factories, thus removing the implementation details from the client.

Abstract Factory Example UML Diagram

See this UML diagram of an Abstract Furniture Factory implementation that returns chairs and tables.

Abstract Furniture Factory

Source Code

./abstract_factory/client.py

1
2
3
4
5
6
7
8
9
"Abstract Factory Use Case Example Code"

from furniture_factory import FurnitureFactory

FURNITURE = FurnitureFactory.get_furniture("SmallChair")
print(f"{FURNITURE.__class__} : {FURNITURE.get_dimensions()}")

FURNITURE = FurnitureFactory.get_furniture("MediumTable")
print(f"{FURNITURE.__class__} : {FURNITURE.get_dimensions()}")

./abstract_factory/furniture_factory.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
# pylint: disable=too-few-public-methods
"Abstract Furniture Factory"
from interface_furniture_factory import IFurnitureFactory
from chair_factory import ChairFactory
from table_factory import TableFactory

class FurnitureFactory(IFurnitureFactory):
    "The Abstract Factory Concrete Class"

    @staticmethod
    def get_furniture(furniture):
        "Static get_factory method"
        try:
            if furniture in ['SmallChair', 'MediumChair', 'BigChair']:
                return ChairFactory.get_chair(furniture)
            if furniture in ['SmallTable', 'MediumTable', 'BigTable']:
                return TableFactory.get_table(furniture)
            raise Exception('No Factory Found')
        except Exception as _e:
            print(_e)
        return None

./abstract_factory/interface_furniture_factory.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# pylint: disable=too-few-public-methods
"The Abstract Factory Interface"

from abc import ABCMeta, abstractmethod

class IFurnitureFactory(metaclass=ABCMeta):
    "Abstract Furniture Factory Interface"

    @staticmethod
    @abstractmethod
    def get_furniture(furniture):
        "The static Abstract factory interface method"

./abstract_factory/chair_factory.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
"The Factory Class"

from small_chair import SmallChair
from medium_chair import MediumChair
from big_chair import BigChair

class ChairFactory:  # pylint: disable=too-few-public-methods
    "The Factory Class"

    @staticmethod
    def get_chair(chair):
        "A static method to get a chair"
        try:
            if chair == 'BigChair':
                return BigChair()
            if chair == 'MediumChair':
                return MediumChair()
            if chair == 'SmallChair':
                return SmallChair()
            raise Exception('Chair Not Found')
        except Exception as _e:
            print(_e)
        return None

./abstract_factory/interface_chair.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# pylint: disable=too-few-public-methods
"The Chair Interface"
from abc import ABCMeta, abstractmethod

class IChair(metaclass=ABCMeta):
    "The Chair Interface (Product)"

    @staticmethod
    @abstractmethod
    def get_dimensions():
        "A static interface method"

./abstract_factory/small_chair.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
"A Class of Chair"
from interface_chair import IChair

class SmallChair(IChair):  # pylint: disable=too-few-public-methods
    "The Small Chair Concrete Class implements the IChair interface"

    def __init__(self):
        self._height = 40
        self._width = 40
        self._depth = 40

    def get_dimensions(self):
        return {
            "width": self._width,
            "depth": self._depth,
            "height": self._height
        }

./abstract_factory/medium_chair.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
"A Class of Chair"
from interface_chair import IChair

class MediumChair(IChair):  # pylint: disable=too-few-public-methods
    """The Medium Chair Concrete Class implements the IChair interface"""

    def __init__(self):
        self._height = 60
        self._width = 60
        self._depth = 60

    def get_dimensions(self):
        return {
            "width": self._width,
            "depth": self._depth,
            "height": self._height
        }

./abstract_factory/big_chair.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
"A Class of Chair"
from interface_chair import IChair

class BigChair(IChair):  # pylint: disable=too-few-public-methods
    "The Big Chair Concrete Class that implements the IChair interface"

    def __init__(self):
        self._height = 80
        self._width = 80
        self._depth = 80

    def get_dimensions(self):
        return {
            "width": self._width,
            "depth": self._depth,
            "height": self._height
        }

./abstract_factory/table_factory.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
"The Factory Class"
from small_table import SmallTable
from medium_table import MediumTable
from big_table import BigTable

class TableFactory:  # pylint: disable=too-few-public-methods
    "The Factory Class"

    @staticmethod
    def get_table(table):
        "A static method to get a table"
        try:
            if table == 'BigTable':
                return BigTable()
            if table == 'MediumTable':
                return MediumTable()
            if table == 'SmallTable':
                return SmallTable()
            raise Exception('Table Not Found')
        except Exception as _e:
            print(_e)
        return None

./abstract_factory/interface_table.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# pylint: disable=too-few-public-methods
"The Table Interface"
from abc import ABCMeta, abstractmethod

class ITable(metaclass=ABCMeta):
    "The Table Interface (Product)"

    @staticmethod
    @abstractmethod
    def get_dimensions():
        "A static interface method"

./abstract_factory/small_table.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
"A Class of Table"
from interface_table import ITable

class SmallTable(ITable):  # pylint: disable=too-few-public-methods
    "The Small Table Concrete Class implements the ITable interface"

    def __init__(self):
        self._height = 60
        self._width = 100
        self._depth = 60

    def get_dimensions(self):
        return {
            "width": self._width,
            "depth": self._depth,
            "height": self._height
        }

./abstract_factory/medium_table.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
"A Class of Table"
from interface_table import ITable

class MediumTable(ITable):  # pylint: disable=too-few-public-methods
    "The Medium Table Concrete Class implements the ITable interface"

    def __init__(self):
        self._height = 60
        self._width = 110
        self._depth = 70

    def get_dimensions(self):
        return {
            "width": self._width,
            "depth": self._depth,
            "height": self._height
        }

./abstract_factory/big_table.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
"A Class of Table"
from interface_table import ITable

class BigTable(ITable):  # pylint: disable=too-few-public-methods
    "The Big Chair Concrete Class implements the ITable interface"

    def __init__(self):
        self._height = 60
        self._width = 120
        self._depth = 80

    def get_dimensions(self):
        return {
            "width": self._width,
            "depth": self._depth,
            "height": self._height
        }

Output

python ./abstract_factory/client.py
<class 'small_chair.SmallChair'> : {'width': 40, 'depth': 40, 'height': 40}
<class 'medium_table.MediumTable'> : {'width': 110, 'depth': 70, 'height': 60}

SBCODE Editor

<>

New Coding Concepts

Exception Handling

Your Python code may produce errors. It happens to everybody. It is hard to foresee all possible errors, but you can try to handle them in case anyway.

Use the Try, Except and optional finally keywords to manage error handling.

In the example code, if no chair or table is returned, an Exception error is raised, and it includes a text string that can be read and written to the console.

Within your code you can use the raise keyword to trigger Python built in exceptions or even create your own.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
def get_furniture(furniture):
    "Static get_factory method"
    try:
        if furniture in ['SmallChair', 'MediumChair', 'BigChair']:
            return ChairFactory.get_chair(furniture)
        if furniture in ['SmallTable', 'MediumTable', 'BigTable']:
            return TableFactory.get_table(furniture)
        raise Exception('No Factory Found')
    except Exception as _e:
        print(_e)
    return None

If WoodenTable is requested from the factory, it will print No Factory Found

You don't need to always raise an exception to make one happen. In that case you can handle the possibility of any type of error using just try and except, with the optional finally if you need it.

1
2
3
4
5
6
try:
  print(my_var)
except:
  print("An unknown error Occurred")
finally:
  print("This is optional and will get called even if there is no error")

The above code produces the message An Error Occurred because my_var is not defined.

The try/except allows the program to continue running, as can be verified by the line printed in the finally statement. So, this has given you the opportunity to manage any unforeseen errors any way you wish.

Alternatively, if your code didn't include the try/except and optional finally statements, the Python interpreter would return the error NameError: name 'my_var' is not defined and the program will crash at that line.

Also note how the default Python inbuilt error starts with NameError. You can handle this specific error explicitly using an extra except keyword.

1
2
3
4
5
6
7
8
try:
    print(my_var)
except NameError:
    print("There was a `NameError`")
except:
    print("An unknown error Occurred")
finally:
    print("This is optional and will get called even if there is no error")

You can add exception handling for as many types of errors as you wish.

Python Errors and Exceptions : https://docs.python.org/3/tutorial/errors.html

Summary

  • Use when you want to provide a library of relatively similar products from multiple different factories.
  • You want the system to be independent of how the products are created.
  • It fulfills all the same use cases as the Factory method, but is a factory for creational pattern type methods.
  • The client implements the abstract factory interface, rather than all the internal logic and Factories. This allows the possibility of creating a library that can be imported for using the Abstract Factory.
  • The Abstract Factory defers the creation of the final products/objects to its concrete factory subclasses.
  • You want to enforce consistent interfaces across products.
  • You want the possibility to exchange product families.