Abstract Factory Design Pattern
Video Lecture
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
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.
Source Code
./abstract_factory/client.py
| "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
| # 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
| # 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.
| 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.
| 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.
| 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.