Explore how UML diagrams represent structural design patterns like Adapter, Decorator, Composite, Facade, and Proxy, making their components and relationships explicit for better software design.
In the realm of software design, understanding the structural design patterns is crucial for creating systems that are not only efficient but also easy to understand and maintain. Structural patterns are about organizing different classes and objects to form larger structures, providing new functionality. This section will delve into how Unified Modeling Language (UML) can be effectively used to represent these patterns, focusing on the Adapter, Decorator, Composite, Facade, and Proxy patterns.
Structural design patterns are concerned with how classes and objects are composed to form larger structures. They are essential for defining clear and efficient relationships between entities in a software system. Let’s explore each of these patterns in detail, using UML diagrams to illustrate their structure and relationships.
The Adapter pattern allows incompatible interfaces to work together. It acts as a bridge between two incompatible interfaces by converting the interface of a class into another interface the client expects.
In UML, the Adapter pattern is depicted by showing how the Adapter class implements the Target interface and uses an instance of the Adaptee class.
classDiagram class Target { +request() } class Adapter { -Adaptee adaptee +request() } class Adaptee { +specificRequest() } Target <|.. Adapter Adapter o-- Adaptee
class Target:
def request(self):
return "Target: The default target's behavior."
class Adaptee:
def specific_request(self):
return ".eetpadA eht fo roivaheb laicepS"
class Adapter(Target):
def __init__(self, adaptee: Adaptee):
self.adaptee = adaptee
def request(self):
return f"Adapter: (TRANSLATED) {self.adaptee.specific_request()[::-1]}"
def client_code(target: Target):
print(target.request())
print("Client: I can work just fine with the Target objects:")
target = Target()
client_code(target)
print("\nClient: The Adaptee class has a weird interface. See, I don't understand it:")
adaptee = Adaptee()
print(f"Adaptee: {adaptee.specific_request()}")
print("\nClient: But I can work with it via the Adapter:")
adapter = Adapter(adaptee)
client_code(adapter)
The Decorator pattern adds new functionality to an existing object without altering its structure. This pattern creates a set of decorator classes that are used to wrap concrete components.
The UML diagram for the Decorator pattern shows how decorators are related to the component they enhance.
classDiagram class Component { +operation() } class ConcreteComponent { +operation() } class Decorator { #Component component +operation() } class ConcreteDecoratorA { +operation() } class ConcreteDecoratorB { +operation() } Component <|-- ConcreteComponent Component <|-- Decorator Decorator <|-- ConcreteDecoratorA Decorator <|-- ConcreteDecoratorB Decorator o--> Component
// Component
class Coffee {
cost() {
return 5;
}
}
// Decorator
class MilkDecorator {
constructor(coffee) {
this.coffee = coffee;
}
cost() {
return this.coffee.cost() + 1;
}
}
// Another Decorator
class SugarDecorator {
constructor(coffee) {
this.coffee = coffee;
}
cost() {
return this.coffee.cost() + 0.5;
}
}
// Usage
let coffee = new Coffee();
console.log(`Cost of plain coffee: $${coffee.cost()}`);
coffee = new MilkDecorator(coffee);
console.log(`Cost with milk: $${coffee.cost()}`);
coffee = new SugarDecorator(coffee);
console.log(`Cost with milk and sugar: $${coffee.cost()}`);
The Composite pattern is used to treat individual objects and compositions of objects uniformly. It composes objects into tree structures to represent part-whole hierarchies.
The UML diagram for the Composite pattern shows the recursive composition of components.
classDiagram class Component { +operation() } class Leaf { +operation() } class Composite { +add(Component) +remove(Component) +operation() } Component <|-- Leaf Component <|-- Composite Composite o-- Component
class Component:
def operation(self):
pass
class Leaf(Component):
def operation(self):
return "Leaf"
class Composite(Component):
def __init__(self):
self._children = []
def add(self, component):
self._children.append(component)
def remove(self, component):
self._children.remove(component)
def operation(self):
results = []
for child in self._children:
results.append(child.operation())
return f"Branch({'+'.join(results)})"
def client_code(component: Component):
print(f"RESULT: {component.operation()}")
leaf = Leaf()
print("Client: I've got a simple component:")
client_code(leaf)
tree = Composite()
branch1 = Composite()
branch1.add(Leaf())
branch1.add(Leaf())
branch2 = Composite()
branch2.add(Leaf())
tree.add(branch1)
tree.add(branch2)
print("Client: Now I've got a composite tree:")
client_code(tree)
The Facade pattern provides a simplified interface to a complex subsystem. It defines a higher-level interface that makes the subsystem easier to use.
The UML diagram for the Facade pattern shows the Facade class interacting with subsystems.
classDiagram class Facade { +operation() } class SubsystemA { +operationA() } class SubsystemB { +operationB() } class SubsystemC { +operationC() } Facade o-- SubsystemA Facade o-- SubsystemB Facade o-- SubsystemC
class SubsystemA {
operationA() {
return "SubsystemA: Ready!\n";
}
}
class SubsystemB {
operationB() {
return "SubsystemB: Go!\n";
}
}
class SubsystemC {
operationC() {
return "SubsystemC: Fire!\n";
}
}
class Facade {
constructor() {
this.subsystemA = new SubsystemA();
this.subsystemB = new SubsystemB();
this.subsystemC = new SubsystemC();
}
operation() {
let result = "Facade initializes subsystems:\n";
result += this.subsystemA.operationA();
result += this.subsystemB.operationB();
result += this.subsystemC.operationC();
return result;
}
}
// Client code
const facade = new Facade();
console.log(facade.operation());
The Proxy pattern provides a surrogate or placeholder for another object to control access to it. It can be used for lazy initialization, logging, access control, and more.
The UML diagram for the Proxy pattern shows the Proxy class implementing the same interface as the RealSubject.
classDiagram class Subject { +request() } class RealSubject { +request() } class Proxy { -RealSubject realSubject +request() } Subject <|.. RealSubject Subject <|.. Proxy Proxy o-- RealSubject
class Subject:
def request(self):
pass
class RealSubject(Subject):
def request(self):
return "RealSubject: Handling request."
class Proxy(Subject):
def __init__(self, real_subject: RealSubject):
self._real_subject = real_subject
def request(self):
if self.check_access():
result = self._real_subject.request()
self.log_access()
return result
def check_access(self):
print("Proxy: Checking access prior to firing a real request.")
return True
def log_access(self):
print("Proxy: Logging the time of request.")
def client_code(subject: Subject):
print(subject.request())
print("Client: Executing the client code with a real subject:")
real_subject = RealSubject()
client_code(real_subject)
print("\nClient: Executing the same client code with a proxy:")
proxy = Proxy(real_subject)
client_code(proxy)
Structural design patterns play a vital role in defining how classes and objects are organized to form larger structures. By using UML diagrams, we can clearly represent the relationships and interactions within these patterns, making them easier to understand and implement. Understanding these patterns and their UML representations is crucial for designing robust and maintainable software systems.