Explore how to represent creational design patterns using UML diagrams, focusing on key patterns like Singleton, Factory Method, Abstract Factory, Builder, and Prototype. Learn through detailed class and sequence diagrams, supported by code examples.
Understanding creational design patterns is crucial for any software developer aiming to create scalable and maintainable systems. These patterns focus on the best ways to create objects, which is a fundamental aspect of software design. In this section, we will delve into how to represent these patterns using Unified Modeling Language (UML) diagrams. UML is a powerful tool that helps visualize the architecture of software systems, making it easier to understand and implement design patterns.
Creational patterns abstract the instantiation process, making the system independent of how its objects are created, composed, and represented. They provide various object creation mechanisms, which increase flexibility and reuse of existing code. The key creational patterns we will explore include:
UML diagrams serve as a blueprint for software systems. They help in visualizing the structure and behavior of a system, which is essential when dealing with complex design patterns. By using class diagrams, we can represent the static structure of the system, showing the classes involved and their relationships. Sequence diagrams, on the other hand, help us understand the dynamic behavior, illustrating how objects interact in a particular scenario.
The Singleton pattern ensures that a class has only one instance and provides a global point of access to it. This pattern is particularly useful when exactly one object is needed to coordinate actions across the system.
In the Singleton pattern, the class diagram highlights the static instance and the private constructor, ensuring that the class cannot be instantiated from outside.
classDiagram class Singleton { -static instance : Singleton -Singleton() +getInstance() : Singleton }
class Singleton:
_instance = None
def __new__(cls):
if cls._instance is None:
cls._instance = super(Singleton, cls).__new__(cls)
return cls._instance
singleton1 = Singleton()
singleton2 = Singleton()
print(singleton1 is singleton2) # Output: True
In this Python example, the __new__
method ensures that only one instance of the class is created. The is
operator confirms that both singleton1
and singleton2
refer to the same instance.
The Factory Method pattern defines an interface for creating an object but lets subclasses alter the type of objects that will be created. It allows a class to defer instantiation to subclasses.
The class diagram for the Factory Method pattern shows the Creator class and its subclasses, the ConcreteCreators. Each ConcreteCreator overrides the factory method to produce an instance of a ConcreteProduct.
classDiagram class Product { } class ConcreteProductA { } class ConcreteProductB { } class Creator { +factoryMethod() : Product } class ConcreteCreatorA { +factoryMethod() : Product } class ConcreteCreatorB { +factoryMethod() : Product } Product <|-- ConcreteProductA Product <|-- ConcreteProductB Creator <|-- ConcreteCreatorA Creator <|-- ConcreteCreatorB Creator o--> Product
class Product {
constructor(name) {
this.name = name;
}
}
class ConcreteProductA extends Product {
constructor() {
super('ConcreteProductA');
}
}
class ConcreteProductB extends Product {
constructor() {
super('ConcreteProductB');
}
}
class Creator {
factoryMethod() {
return new Product('DefaultProduct');
}
}
class ConcreteCreatorA extends Creator {
factoryMethod() {
return new ConcreteProductA();
}
}
class ConcreteCreatorB extends Creator {
factoryMethod() {
return new ConcreteProductB();
}
}
// Usage
const creatorA = new ConcreteCreatorA();
const productA = creatorA.factoryMethod();
console.log(productA.name); // Output: ConcreteProductA
const creatorB = new ConcreteCreatorB();
const productB = creatorB.factoryMethod();
console.log(productB.name); // Output: ConcreteProductB
In this JavaScript example, the ConcreteCreatorA
and ConcreteCreatorB
classes override the factoryMethod
to instantiate and return specific products.
The Abstract Factory pattern provides an interface for creating families of related or dependent objects without specifying their concrete classes. It is useful when a system needs to be independent of how its products are created.
The class diagram illustrates the AbstractFactory interface and its ConcreteFactories, each responsible for creating a family of related products.
classDiagram class AbstractFactory { +createProductA() : AbstractProductA +createProductB() : AbstractProductB } class ConcreteFactory1 { +createProductA() : ProductA1 +createProductB() : ProductB1 } class ConcreteFactory2 { +createProductA() : ProductA2 +createProductB() : ProductB2 } class AbstractProductA { } class AbstractProductB { } class ProductA1 { } class ProductA2 { } class ProductB1 { } class ProductB2 { } AbstractFactory <|-- ConcreteFactory1 AbstractFactory <|-- ConcreteFactory2 AbstractProductA <|-- ProductA1 AbstractProductA <|-- ProductA2 AbstractProductB <|-- ProductB1 AbstractProductB <|-- ProductB2 ConcreteFactory1 o--> ProductA1 ConcreteFactory1 o--> ProductB1 ConcreteFactory2 o--> ProductA2 ConcreteFactory2 o--> ProductB2
class AbstractFactory:
def create_product_a(self):
pass
def create_product_b(self):
pass
class ConcreteFactory1(AbstractFactory):
def create_product_a(self):
return ProductA1()
def create_product_b(self):
return ProductB1()
class ConcreteFactory2(AbstractFactory):
def create_product_a(self):
return ProductA2()
def create_product_b(self):
return ProductB2()
class AbstractProductA:
pass
class AbstractProductB:
pass
class ProductA1(AbstractProductA):
pass
class ProductA2(AbstractProductA):
pass
class ProductB1(AbstractProductB):
pass
class ProductB2(AbstractProductB):
pass
factory1 = ConcreteFactory1()
product_a1 = factory1.create_product_a()
product_b1 = factory1.create_product_b()
factory2 = ConcreteFactory2()
product_a2 = factory2.create_product_a()
product_b2 = factory2.create_product_b()
In this Python example, ConcreteFactory1
and ConcreteFactory2
implement the AbstractFactory
interface to create products from two different product families.
The Builder pattern separates the construction of a complex object from its representation, allowing the same construction process to create different representations.
The class diagram shows the Builder interface and its ConcreteBuilders, each responsible for constructing and assembling parts of the product.
classDiagram class Builder { +buildPartA() : void +buildPartB() : void +getResult() : Product } class ConcreteBuilder1 { +buildPartA() : void +buildPartB() : void +getResult() : Product } class ConcreteBuilder2 { +buildPartA() : void +buildPartB() : void +getResult() : Product } class Director { +construct() : void } class Product { } Builder <|-- ConcreteBuilder1 Builder <|-- ConcreteBuilder2 Director o--> Builder ConcreteBuilder1 o--> Product ConcreteBuilder2 o--> Product
class Product {
constructor() {
this.parts = [];
}
addPart(part) {
this.parts.push(part);
}
show() {
console.log('Product parts: ' + this.parts.join(', '));
}
}
class Builder {
buildPartA() {}
buildPartB() {}
getResult() {}
}
class ConcreteBuilder1 extends Builder {
constructor() {
super();
this.product = new Product();
}
buildPartA() {
this.product.addPart('PartA1');
}
buildPartB() {
this.product.addPart('PartB1');
}
getResult() {
return this.product;
}
}
class ConcreteBuilder2 extends Builder {
constructor() {
super();
this.product = new Product();
}
buildPartA() {
this.product.addPart('PartA2');
}
buildPartB() {
this.product.addPart('PartB2');
}
getResult() {
return this.product;
}
}
class Director {
construct(builder) {
builder.buildPartA();
builder.buildPartB();
}
}
// Usage
const director = new Director();
const builder1 = new ConcreteBuilder1();
director.construct(builder1);
const product1 = builder1.getResult();
product1.show(); // Output: Product parts: PartA1, PartB1
const builder2 = new ConcreteBuilder2();
director.construct(builder2);
const product2 = builder2.getResult();
product2.show(); // Output: Product parts: PartA2, PartB2
In this JavaScript example, the Director
class uses the Builder
interface to construct a product by delegating the building process to ConcreteBuilder1
and ConcreteBuilder2
.
The Prototype pattern is used to create a new object by copying an existing object, known as the prototype. This pattern is useful when the cost of creating a new object is more expensive than cloning.
The class diagram illustrates the Prototype interface and its ConcretePrototypes, each capable of cloning itself.
classDiagram class Prototype { +clone() : Prototype } class ConcretePrototype1 { +clone() : ConcretePrototype1 } class ConcretePrototype2 { +clone() : ConcretePrototype2 } Prototype <|-- ConcretePrototype1 Prototype <|-- ConcretePrototype2
import copy
class Prototype:
def clone(self):
pass
class ConcretePrototype1(Prototype):
def __init__(self, value):
self.value = value
def clone(self):
return copy.deepcopy(self)
class ConcretePrototype2(Prototype):
def __init__(self, value):
self.value = value
def clone(self):
return copy.deepcopy(self)
prototype1 = ConcretePrototype1('Value1')
clone1 = prototype1.clone()
print(clone1.value) # Output: Value1
prototype2 = ConcretePrototype2('Value2')
clone2 = prototype2.clone()
print(clone2.value) # Output: Value2
In this Python example, the clone
method uses deepcopy
to create a new instance of the object with the same state as the original.
Visualizing creational patterns with UML diagrams provides a clear and structured way to understand and implement these patterns. By representing the static and dynamic aspects of these patterns, developers can plan their implementation more effectively, ensuring that the design is robust and scalable.
The examples and diagrams provided in this section illustrate how each pattern can be applied in real-world scenarios, making it easier for developers to choose the right pattern for their specific needs.
By avoiding these pitfalls and following best practices, you can leverage the power of design patterns to create flexible and maintainable software systems.