Creational Design Patterns - Prototype, Factory Method and Abstract Factory
Table of Content
Prototype
- is a creational design pattern that can be used to create objects that are similar to each other.
- The pattern is used to avoid the cost of creating new objects by cloning an existing object and avoiding dependencies on the class of the object that needs to be cloned.
Factory
- is a creational design pattern that can be used to create objects without specifying the exact class of the object that will be created.
- The pattern is used to avoid dependencies on the class of the object that needs to be created.
Prototype
- allows us to hide the complexity of making new instances from the client.
- The concept is to copy an existing object rather than creating a new instance from scratch, something that may include costly operations.
- The existing object acts as a prototype and contains the state of the object. The newly copied object may change same properties only if required.
-
This approach saves costly resources and time, especially when object creation is a heavy process.
- Let us say we have to create a new
User
API and we want to test it. To test it, we need to create a new user. We can create a new user by using thenew
keyword.
1
User user = new User("John", "Doe", "john@doe.in", "1234567890");
- We might be calling a separate API to get these random values for the user. So each time we want to create a new user we have to call the API.
- Instead, we can create a new user by cloning an existing user and modifying the fields that are necessary. This way we can avoid calling the API each time we want to create a new user.
- To clone an existing user, we have to implement a common interface for all the user objects
clone()
.
1
2
3
4
5
6
7
8
public abstract class User {
public abstract User clone();
}
...
User user = new User("John", "Doe", "john@doe.in", "1234567890");
User user2 = user.clone();
user2.setId(2);
- Apart from reducing the cost of creating new objects, the prototype pattern also helps in reducing the complexity of creating new objects.
- The client code does not have to deal with the complexity of creating new objects. It can simply clone the existing object and modify it as per its needs.
- The client code does not have a dependency on the class of the object that it is cloning.
Prototype Registry
- prototype pattern can be extended to use a registry of pre-defined prototypes.
- The registry can be used to store a set of pre-defined prototypes. The client code can then request a clone of a prototype from the registry instead of creating a new object from scratch.
- The registry can be implemented as a key-value store where the key is the name of the prototype and the value is the prototype object.
Example
- we might want to create different types of users. A user with a Student role, a user with a Teacher role, and a user with an Admin role.
- Each such different type of user might have some fields that are specific to the type so the fields to be copied might be different.
- We can create a registry of pre-defined prototypes for each of these roles
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
...
interface UserRegistry {
User getPrototype(UserRole role);
void addPrototype(UserRole role, User user);
}
class UserRegistryImpl implements UserRegistry {
private Map<UserRole, User> registry = new HashMap<>();
@Override
public User getPrototype(UserRole role) {
return registry.get(role).clone();
}
@Override
public void addPrototype(UserRole role, User user) {
registry.put(role, user);
}
}
...
UserRegistry registry = new UserRegistryImpl();
registry.addPrototype(UserRole.STUDENT, new Student("John", "Doe", "john@doe.in", "1234567890", UserRole.STUDENT, "CS"));
User user = registry.getPrototype(UserRole.STUDENT);
user.setId(1);
Prototype Pattern in Python
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
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
from abc import ABC, abstractmethod
from copy import deepcopy
from enum import Enum
class ObjectClonable(ABC):
@abstractmethod
def clone(self):
pass
class MLModel(ObjectClonable):
def __init__(self, model_type, description, training_split, validation_split, alpha, beta):
self._model_type = model_type
self._description = description
self._training_split = training_split
self._validation_split = validation_split
self._alpha = alpha
self._beta = beta
def clone(self):
return deepcopy(self)
# Getter and Setter methods
@property
def model_type(self):
return self._model_type
@model_type.setter
def model_type(self, model_type):
self._model_type = model_type
@property
def description(self):
return self._description
@description.setter
def description(self, description):
self._description = description
@property
def training_split(self):
return self._training_split
@training_split.setter
def training_split(self, training_split):
self._training_split = training_split
@property
def validation_split(self):
return self._validation_split
@validation_split.setter
def validation_split(self, validation_split):
self._validation_split = validation_split
@property
def alpha(self):
return self._alpha
@alpha.setter
def alpha(self, alpha):
self._alpha = alpha
@property
def beta(self):
return self._beta
@beta.setter
def beta(self, beta):
self._beta = beta
def __str__(self):
return f'MLModel(type={self._model_type}, description={self._description}, ' \
f'training_split={self._training_split}, validation_split={self._validation_split}, ' \
f'alpha={self._alpha}, beta={self._beta})'
class ModelType(Enum):
LR = "LR"
SVM = "SVM"
DT = "DT"
# Step 4: Create and populate registry
class ModelRegistry:
def __init__(self):
self._models = {}
def register_model(self, model):
self._models[model.model_type] = model
def get_model(self, model_type):
model = self._models.get(model_type)
return model.clone() if model else None
# Example usage
if __name__ == "__main__":
# Create models
lr_model = MLModel(ModelType.LR, "Linear Regression Model", 0.7, 0.3, 0.01, 0.1)
svm_model = MLModel(ModelType.SVM, "Support Vector Machine Model", 0.6, 0.4, 0.02, 0.2)
# Register models
registry = ModelRegistry()
registry.register_model(lr_model)
registry.register_model(svm_model)
# Retrieve and clone models
cloned_lr_model = registry.get_model(ModelType.LR)
cloned_lr_model.description = "Linear Regression Model Cloned"
cloned_svm_model = registry.get_model(ModelType.SVM)
cloned_svm_model.description = "Support Vector Machine Model Cloned"
print(lr_model)
print(cloned_lr_model)
print(svm_model)
print(cloned_svm_model)
Prototype Pattern in Golang
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
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
package prototype
type ObjectClonable interface {
Clone() *MLModel
}
type MLModel struct {
modelType ModelType
description string
trainingSplit float64
validationSplit float64
alpha float64
beta float64
}
func (m *MLModel) Clone() *MLModel {
return &MLModel{
modelType: m.modelType,
description: m.description,
trainingSplit: m.trainingSplit,
validationSplit: m.validationSplit,
alpha: m.alpha,
beta: m.beta,
}
}
// Getter and Setter methods
func (m *MLModel) GetModelType() ModelType {
return m.modelType
}
func (m *MLModel) SetModelType(modelType ModelType) {
m.modelType = modelType
}
func (m *MLModel) GetDescription() string {
return m.description
}
func (m *MLModel) SetDescription(description string) {
m.description = description
}
func (m *MLModel) GetTrainingSplit() float64 {
return m.trainingSplit
}
func (m *MLModel) SetTrainingSplit(trainingSplit float64) {
m.trainingSplit = trainingSplit
}
func (m *MLModel) GetValidationSplit() float64 {
return m.validationSplit
}
func (m *MLModel) SetValidationSplit(validationSplit float64) {
m.validationSplit = validationSplit
}
func (m *MLModel) GetAlpha() float64 {
return m.alpha
}
func (m *MLModel) SetAlpha(alpha float64) {
m.alpha = alpha
}
func (m *MLModel) GetBeta() float64 {
return m.beta
}
func (m *MLModel) SetBeta(beta float64) {
m.beta = beta
}
type ModelType string
const (
LR ModelType = "LR"
SVM ModelType = "SVM"
DT ModelType = "DT"
)
type ModelRegistry struct {
models map[ModelType]*MLModel
}
func NewModelRegistry() *ModelRegistry {
return &ModelRegistry{
models: make(map[ModelType]*MLModel),
}
}
func (r *ModelRegistry) RegisterModel(model *MLModel) {
r.models[model.GetModelType()] = model
}
func (r *ModelRegistry) GetModel(modelType ModelType) *MLModel {
if model, exists := r.models[modelType]; exists {
return model.Clone()
}
return nil
}
Factory
- is a creational pattern that uses factory methods to deal with the problem of creating objects without having to specify the exact class of the object that will be created.
- This is done by creating objects by calling a factory method—either specified in an interface and implemented by child classes, or implemented in a base class and optionally overridden by derived classes—rather than by calling a constructor.
- The client code can request an object from a factory object without having to know the class of the object that will be returned. The factory object can create the object and return it to the client code.
Simple Factory
- is a creational pattern that provides a static method for creating objects.
- The method can be used to create objects without having to specify the exact class of the object that will be created. This is done by creating a factory class that contains a static method for creating objects.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class UserFactory {
public static User createUser(UserRole role) {
switch (role) {
case STUDENT:
return new Student("John", "Doe");
case TEACHER:
return new Teacher("John", "Doe");
case ADMIN:
return new Admin("John", "Doe");
}
}
}
...
User user = UserFactory.createUser(UserRole.STUDENT);
- The complete steps to implement the simple factory pattern are:
- Factory class - Create a factory class that contains a static method for creating objects
- Conditional - Use a conditional statement to create the object based on the input.
- Request - Request an object from the factory class without having to know the class of the object that will be returned
Factory Method
- simple factory method is easy to implement, but it has a few drawbacks.
- The factory class is not extensible. If we want to add a new type of user, we will have to modify the factory class.
- Also, the factory class is not reusable. If we want to create a factory for creating different types of objects, we will have to create a new factory class.
-
To overcome these drawbacks, we can use the factory method pattern.
- In the factory method the responsibility of creating the object is shifted to the child classes.
- The factory method is implemented in the base class and the child classes can override the factory method to create objects of their own type.
- The factory method is also known as the virtual constructor.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@AllArgsContructor
abstract class UserFactory {
public abstract User createUser(String firstName, String lastName);
}
class StudentFactory extends UserFactory {
@Override
public User createUser(String firstName, String lastName) {
return new Student(firstName, lastName);
}
}
...
UserFactory factory = new StudentFactory();
User user = factory.createUser("John", "Doe");
- The complete steps to implement the factory method pattern are:
- Base factory interface - Create a factory class that contains a method for creating objects.
- Child factory class - Create a child class that extends the base factory class and overrides the factory method to create objects of its own type.
- Request - Request an object from the factory class without having to know the class of the object that will be returned.
Factory Method in Python
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
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
from abc import ABC, abstractmethod
class Button(ABC):
def __init__(self, border: float):
self._border = border
@property
def border(self) -> float:
return self._border
@border.setter
def border(self, value: float):
self._border = value
@abstractmethod
def render(self):
pass
@abstractmethod
def onClick(self):
pass
class ButtonFactory(ABC):
@abstractmethod
def createButton(self, border: float, radius: float, length: float) -> Button:
pass
class RoundButton(Button):
def __init__(self, border: float, radius: float):
super().__init__(border)
self._radius = radius
@property
def radius(self) -> float:
return self._radius
def onClick(self):
print("Round Button was clicked!")
def render(self):
print("Rendered!")
class RoundButtonFactory(ButtonFactory):
def createButton(self, border: float, radius: float, length: float) -> Button:
return RoundButton(border, radius)
class SquareButton(Button):
def __init__(self, border: float, length: float):
super().__init__(border)
self._length = length
@property
def length(self) -> float:
return self._length
def onClick(self):
print("Square Button was clicked!")
def render(self):
print("Rendered!")
class SquareButtonFactory(ButtonFactory):
def createButton(self, border: float, radius: float, length: float) -> Button:
return SquareButton(border, length)
def client_code(factory: ButtonFactory):
button = factory.createButton(border=1.0, radius=5.0, length=10.0)
button.render()
button.onClick()
def main():
round_button_factory = RoundButtonFactory()
print("Using RoundButtonFactory:")
client_code(round_button_factory)
square_button_factory = SquareButtonFactory()
print("\nUsing SquareButtonFactory:")
client_code(square_button_factory)
if __name__ == "__main__":
main()
Factory Method in Golang
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
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
package factorymethod
import "fmt"
// Button is the abstract product
type Button interface {
Render()
OnClick()
GetBorder() float64
SetBorder(border float64)
}
// RoundButton is a concrete product
type RoundButton struct {
border float64
radius float64
}
func (b *RoundButton) Render() {
fmt.Println("Rendered RoundButton!")
}
func (b *RoundButton) OnClick() {
fmt.Println("Round Button was clicked!")
}
func (b *RoundButton) GetBorder() float64 {
return b.border
}
func (b *RoundButton) SetBorder(border float64) {
b.border = border
}
func (b *RoundButton) GetRadius() float64 {
return b.radius
}
func (b *RoundButton) SetRadius(radius float64) {
b.radius = radius
}
// SquareButton is a concrete product
type SquareButton struct {
border float64
length float64
}
func (b *SquareButton) Render() {
fmt.Println("Rendered SquareButton!")
}
func (b *SquareButton) OnClick() {
fmt.Println("Square Button was clicked!")
}
func (b *SquareButton) GetBorder() float64 {
return b.border
}
func (b *SquareButton) SetBorder(border float64) {
b.border = border
}
func (b *SquareButton) GetLength() float64 {
return b.length
}
func (b *SquareButton) SetLength(length float64) {
b.length = length
}
// ButtonFactory is the abstract factory
type ButtonFactory interface {
CreateButton(border float64, radius float64, length float64) Button
}
// RoundButtonFactory is a concrete factory
type RoundButtonFactory struct{}
func (f *RoundButtonFactory) CreateButton(border float64, radius float64, length float64) Button {
return &RoundButton{
border: border,
radius: radius,
}
}
// SquareButtonFactory is a concrete factory
type SquareButtonFactory struct{}
func (f *SquareButtonFactory) CreateButton(border float64, radius float64, length float64) Button {
return &SquareButton{
border: border,
length: length,
}
}
func ClientCode(factory ButtonFactory) {
button := factory.CreateButton(1.0, 5.0, 10.0)
button.Render()
button.OnClick()
}
Abstract Factory
- is a creational pattern that provides an interface for creating families of related or dependent objects without specifying their concrete classes
Example
- We have already created a
User
abstract class. Now we will create the concrete classesStudent
andTeacher
. - To restrict the usage of subclasses, we can create factories for each of the concrete classes. The
StudentFactory
will be used to createStudent
objects and theTeacherFactory
will be used to createTeacher
objects.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class StudentFactory {
public User createStudent(String firstName, String lastName) {
return new Student(firstName, lastName);
}
}
class TeacherFactory {
public User createTeacher(String firstName, String lastName) {
return new Teacher(firstName, lastName);
}
}
...
StudentFactory studentFactory = new StudentFactory();
Student student = studentFactory.createStudent("John", "Doe");
TeacherFactory teacherFactory = new TeacherFactory();
Teacher teacher = teacherFactory.createTeacher("John", "Doe");
- But now we have a problem, we can use the factories to create any type of student and teacher. Should a teacher teaching Physics be able to teach a student of Biology class?
- This is where the concept of related or a family of objects comes into play. The
Student
andTeacher
objects are related to each other. - A teacher should only be able to teach a student of the same class. So we can create a factory that can create a family of related objects. The
ClassroomFactory
will be used to createStudent
andTeacher
objects of the same class.
1
2
3
4
abstract class ClassroomFactory {
public abstract Student createStudent(String firstName, String lastName);
public abstract Teacher createTeacher(String firstName, String lastName);
}
- Now we can create concrete factories for each family of related objects that we want to create.
1
2
3
4
5
6
7
8
9
10
11
class BiologyClassroomFactory extends ClassroomFactory {
@Override
public Student createStudent(String firstName, String lastName) {
return new BiologyStudent(firstName, lastName);
}
@Override
public Teacher createTeacher(String firstName, String lastName) {
return new BiologyTeacher(firstName, lastName);
}
}
- The class
ClassroomFactory
is an abstract class that contains the factory methods for creating the objects. - The child classes can override the factory methods to create objects of their own type. The client code can request an object from the factory class without having to know the class of the object that will be returned.
1
2
3
ClassroomFactory factory = new BiologyClassroomFactory();
Student student = factory.createStudent("John", "Doe");
Teacher teacher = factory.createTeacher("John", "Doe");
- The class
ClassroomFactory
becomes our abstract factory that essentially is a factory of factories.
Abstract Factory in Python
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
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
from abc import ABC, abstractmethod
# Abstract Product: Button
class Button(ABC):
def __init__(self, border: float):
self._border = border
@property
def border(self) -> float:
return self._border
@abstractmethod
def render(self):
pass
@abstractmethod
def onClick(self):
pass
# Concrete Product: DarkButton
class DarkButton(Button):
def __init__(self, border: float, radius: float):
super().__init__(border)
self._radius = radius
@property
def radius(self) -> float:
return self._radius
def onClick(self):
print("Dark Btn was clicked!")
def render(self):
print("Rendered!")
# Abstract Product: Radio
class Radio(ABC):
@abstractmethod
def onSelect(self):
pass
@abstractmethod
def render(self):
pass
# Concrete Product: DarkRadio
class DarkRadio(Radio):
def onSelect(self):
print("DarkRadio selected!")
def render(self):
print("DarkRadio rendered!")
# Abstract Factory
class ThemeFactory(ABC):
@abstractmethod
def createButton(self, border: float, length: float, radius: float) -> Button:
pass
@abstractmethod
def createRadio(self) -> Radio:
pass
# Concrete Factory: DarkThemeFactory
class DarkThemeFactory(ThemeFactory):
def createButton(self, border: float, length: float, radius: float) -> Button:
return DarkButton(border, radius)
def createRadio(self) -> Radio:
return DarkRadio()
# Concrete Product: LightButton
class LightButton(Button):
def __init__(self, border: float, length: float):
super().__init__(border)
self._length = length
@property
def length(self) -> float:
return self._length
def onClick(self):
print("Light Btn was clicked!")
def render(self):
print("Rendered!")
# Concrete Product: LightRadio
class LightRadio(Radio):
def onSelect(self):
print("LightRadio selected!")
def render(self):
print("LightRadio rendered!")
# Concrete Factory: LightThemeFactory
class LightThemeFactory(ThemeFactory):
def createButton(self, border: float, length: float, radius: float) -> Button:
return LightButton(border, length)
def createRadio(self) -> Radio:
return LightRadio()
# Client code
def client_code(factory: ThemeFactory):
button = factory.createButton(border=1.0, length=10.0, radius=5.0)
button.render()
button.onClick()
radio = factory.createRadio()
radio.render()
radio.onSelect()
def main():
print("Using DarkThemeFactory:")
dark_factory = DarkThemeFactory()
client_code(dark_factory)
print("\nUsing LightThemeFactory:")
light_factory = LightThemeFactory()
client_code(light_factory)
if __name__ == "__main__":
main()
Abstract Factory in Golang
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
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
package abstractfactory
import "fmt"
// Button is the abstract product
type Button interface {
Render()
OnClick()
GetBorder() float64
}
// DarkButton is a concrete product
type DarkButton struct {
border float64
radius float64
}
func (b *DarkButton) Render() {
fmt.Println("Rendered DarkButton!")
}
func (b *DarkButton) OnClick() {
fmt.Println("Dark Btn was clicked!")
}
func (b *DarkButton) GetBorder() float64 {
return b.border
}
func (b *DarkButton) GetRadius() float64 {
return b.radius
}
// Radio is the abstract product
type Radio interface {
OnSelect()
Render()
}
// DarkRadio is a concrete product
type DarkRadio struct{}
func (r *DarkRadio) OnSelect() {
fmt.Println("DarkRadio selected!")
}
func (r *DarkRadio) Render() {
fmt.Println("DarkRadio rendered!")
}
// LightButton is a concrete product
type LightButton struct {
border float64
length float64
}
func (b *LightButton) Render() {
fmt.Println("Rendered LightButton!")
}
func (b *LightButton) OnClick() {
fmt.Println("Light Btn was clicked!")
}
func (b *LightButton) GetBorder() float64 {
return b.border
}
func (b *LightButton) GetLength() float64 {
return b.length
}
// LightRadio is a concrete product
type LightRadio struct{}
func (r *LightRadio) OnSelect() {
fmt.Println("LightRadio selected!")
}
func (r *LightRadio) Render() {
fmt.Println("LightRadio rendered!")
}
// ThemeFactory is the abstract factory
type ThemeFactory interface {
CreateButton(border, length, radius float64) Button
CreateRadio() Radio
}
// DarkThemeFactory is a concrete factory
type DarkThemeFactory struct{}
func (f *DarkThemeFactory) CreateButton(border, length, radius float64) Button {
return &DarkButton{border: border, radius: radius}
}
func (f *DarkThemeFactory) CreateRadio() Radio {
return &DarkRadio{}
}
// LightThemeFactory is a concrete factory
type LightThemeFactory struct{}
func (f *LightThemeFactory) CreateButton(border, length, radius float64) Button {
return &LightButton{border: border, length: length}
}
func (f *LightThemeFactory) CreateRadio() Radio {
return &LightRadio{}
}
// Client code
func ClientCode(factory ThemeFactory) {
button := factory.CreateButton(1.0, 10.0, 5.0)
button.Render()
button.OnClick()
radio := factory.CreateRadio()
radio.Render()
radio.OnSelect()
}
Advantages of Abstract Factory
- Isolate concrete classes - The client code is not coupled to the concrete classes of the objects that it creates.
- Easy to exchange product families - The client code can request an object from the factory class without having to know the class of the object that will be returned. This makes it easy to exchange product families.
- Promotes consistency among products - The client code can request an object from the factory class without having to know the class of the object that will be returned. This makes it easy to maintain consistency among products.
References: