Structural
Structural#
Structural design patterns are concerned with how classes and objects are composed to form larger structures. They help ensure that when one part of a system changes, the entire structure doesn't need to change. These patterns focus on the composition of classes or objects, making it easier to create relationships between entities.
The Adapter design pattern helps you make incompatible interfaces work together. Instead of modifying existing classes that have different interfaces, you create an adapter that wraps one interface and translates it to another. This keeps both the client code and the adapted class unchanged. As a result, you can integrate legacy components or third-party libraries without altering their source code.
Key Concepts
- Target: The interface that the client expects to work with.
- Adaptee: The existing interface that needs to be adapted.
- Adapter: The class that implements the Target interface and wraps the Adaptee.
- Client: The class that uses the Target interface.
Benefits
- It allows classes with incompatible interfaces to work together seamlessly.
- It promotes code reusability by enabling integration of existing components.
Let's make the Adapter pattern more concrete with a practical example. Imagine we have an old analog guitar tuner that we want to use with a new digital tuner interface. By using the Adapter, we can wrap the analog tuner and make it compatible with the digital interface without modifying either class.
The diagram below illustrates how the Adapter pattern is applied in our guitar tuner scenario: the TunerAdapter wraps the AnalogTuner and implements the DigitalTuner interface, allowing the client to use the old tuner through the new interface:
classDiagram
class DigitalTuner {
<<interface>>
+tune()
}
class TunerAdapter {
-analogTuner: AnalogTuner
+tune()
}
class AnalogTuner {
+tuneAnalog()
}
DigitalTuner <|.. TunerAdapter
TunerAdapter o-- AnalogTuner
Now, let's see how this pattern comes to life in code by implementing a tuner adapter that makes an analog tuner compatible with a digital interface.
// The Target interface
interface DigitalTuner {
void tune();
}
// The Adaptee
class AnalogTuner {
void tuneAnalog() {
System.out.println("Tuning using analog tuner");
}
}
// The Adapter
class TunerAdapter implements DigitalTuner {
private AnalogTuner analogTuner;
public TunerAdapter(AnalogTuner analogTuner) {
this.analogTuner = analogTuner;
}
@Override
public void tune() {
analogTuner.tuneAnalog();
}
}
// Client
public class Main {
public static void main(String[] args) {
AnalogTuner analogTuner = new AnalogTuner();
DigitalTuner digitalTuner = new TunerAdapter(analogTuner);
digitalTuner.tune(); // Output: Tuning using analog tuner
}
}
// The Target interface
interface DigitalTuner {
fun tune()
}
// The Adaptee
class AnalogTuner {
fun tuneAnalog() {
println("Tuning using analog tuner")
}
}
// The Adapter
class TunerAdapter(private val analogTuner: AnalogTuner) : DigitalTuner {
override fun tune() {
analogTuner.tuneAnalog()
}
}
// Client
fun main() {
val analogTuner = AnalogTuner()
val digitalTuner: DigitalTuner = TunerAdapter(analogTuner)
digitalTuner.tune() // Output: Tuning using analog tuner
}
// The Target interface
interface DigitalTuner {
tune(): void;
}
// The Adaptee
class AnalogTuner {
tuneAnalog(): void {
console.log("Tuning using analog tuner");
}
}
// The Adapter
class TunerAdapter implements DigitalTuner {
private analogTuner: AnalogTuner;
constructor(analogTuner: AnalogTuner) {
this.analogTuner = analogTuner;
}
tune(): void {
this.analogTuner.tuneAnalog();
}
}
// Client
const analogTuner = new AnalogTuner();
const digitalTuner: DigitalTuner = new TunerAdapter(analogTuner);
digitalTuner.tune(); // Output: Tuning using analog tuner
// The Target interface
abstract class DigitalTuner {
void tune();
}
// The Adaptee
class AnalogTuner {
void tuneAnalog() {
print("Tuning using analog tuner");
}
}
// The Adapter
class TunerAdapter implements DigitalTuner {
final AnalogTuner _analogTuner;
TunerAdapter(this._analogTuner);
@override
void tune() {
_analogTuner.tuneAnalog();
}
}
// Client
void main() {
final analogTuner = AnalogTuner();
final digitalTuner = TunerAdapter(analogTuner);
digitalTuner.tune(); // Output: Tuning using analog tuner
}
// The Target protocol
protocol DigitalTuner {
func tune()
}
// The Adaptee
class AnalogTuner {
func tuneAnalog() {
print("Tuning using analog tuner")
}
}
// The Adapter
class TunerAdapter: DigitalTuner {
private let analogTuner: AnalogTuner
init(analogTuner: AnalogTuner) {
self.analogTuner = analogTuner
}
func tune() {
analogTuner.tuneAnalog()
}
}
// Client
let analogTuner = AnalogTuner()
let digitalTuner: DigitalTuner = TunerAdapter(analogTuner: analogTuner)
digitalTuner.tune() // Output: Tuning using analog tuner
# The Target interface
from abc import ABC, abstractmethod
class DigitalTuner(ABC):
@abstractmethod
def tune(self):
pass
# The Adaptee
class AnalogTuner:
def tune_analog(self):
print("Tuning using analog tuner")
# The Adapter
class TunerAdapter(DigitalTuner):
def __init__(self, analog_tuner):
self.analog_tuner = analog_tuner
def tune(self):
self.analog_tuner.tune_analog()
# Client
analog_tuner = AnalogTuner()
digital_tuner = TunerAdapter(analog_tuner)
digital_tuner.tune() # Output: Tuning using analog tuner
Summary
The Adapter design pattern allows incompatible interfaces to work together by creating a bridge between them. This pattern is particularly useful when integrating legacy code with new systems or when working with third-party libraries that have different interfaces.
The Bridge design pattern helps you split a large class or closely related classes into two separate hierarchies—abstraction and implementation—that can evolve independently. Instead of creating a monolithic class hierarchy with all possible combinations, you separate the abstraction from its implementation. This keeps both hierarchies decoupled and allows them to vary without affecting each other. As a result, you can combine any abstraction with any implementation at runtime.
Key Concepts
- Abstraction: The main object that clients use. It contains a reference to an Implementation and uses it to do part of its work.
- Implementation: The helper interface that the Abstraction delegates to. Different implementations can be swapped without changing the Abstraction.
- Refined Abstraction: A specific variant of the Abstraction.
- Concrete Implementation: A specific variant of the Implementation.
Benefits
- It separates abstraction from implementation, allowing both to evolve independently.
- It hides implementation details from clients, reducing coupling.
Let's make the Bridge pattern more concrete with a practical example. Imagine we have different types of guitar amplifiers (tube, solid-state) and different effects (reverb, distortion). By using the Bridge, we can combine any amplifier type with any effect type without creating a class for every possible combination.
The diagram below illustrates how the Bridge pattern is applied in our amplifier scenario: the Amplifier abstraction holds a reference to an Effect implementation, allowing any amplifier to work with any effect:
classDiagram
class Amplifier {
<<abstract>>
#effect: Effect
+play()*
}
class TubeAmplifier {
+play()
}
class SolidStateAmplifier {
+play()
}
class Effect {
<<interface>>
+applyEffect()
}
class ReverbEffect {
+applyEffect()
}
class DistortionEffect {
+applyEffect()
}
Amplifier o-- Effect
Amplifier <|-- TubeAmplifier
Amplifier <|-- SolidStateAmplifier
Effect <|.. ReverbEffect
Effect <|.. DistortionEffect
Now, let's see how this pattern comes to life in code by implementing amplifiers that can be paired with different effects using the Bridge approach.
// The Implementation interface
interface Effect {
void applyEffect();
}
// The Concrete implementations
class ReverbEffect implements Effect {
@Override
public void applyEffect() {
System.out.println("Applying reverb effect");
}
}
class DistortionEffect implements Effect {
@Override
public void applyEffect() {
System.out.println("Applying distortion effect");
}
}
// The Abstraction
abstract class Amplifier {
protected Effect effect;
public Amplifier(Effect effect) {
this.effect = effect;
}
abstract void play();
}
// The Refined abstraction
class TubeAmplifier extends Amplifier {
public TubeAmplifier(Effect effect) {
super(effect);
}
@Override
void play() {
System.out.println("Playing through tube amplifier");
effect.applyEffect();
}
}
class SolidStateAmplifier extends Amplifier {
public SolidStateAmplifier(Effect effect) {
super(effect);
}
@Override
void play() {
System.out.println("Playing through solid state amplifier");
effect.applyEffect();
}
}
// Client
public class Main {
public static void main(String[] args) {
Effect reverb = new ReverbEffect();
Effect distortion = new DistortionEffect();
Amplifier tubeAmp = new TubeAmplifier(reverb);
Amplifier solidStateAmp = new SolidStateAmplifier(distortion);
tubeAmp.play();
solidStateAmp.play();
}
}
// The Implementation interface
interface Effect {
fun applyEffect()
}
// The Concrete implementations
class ReverbEffect : Effect {
override fun applyEffect() {
println("Applying reverb effect")
}
}
class DistortionEffect : Effect {
override fun applyEffect() {
println("Applying distortion effect")
}
}
// The Abstraction
abstract class Amplifier(protected val effect: Effect) {
abstract fun play()
}
// The Refined abstraction
class TubeAmplifier(effect: Effect) : Amplifier(effect) {
override fun play() {
println("Playing through tube amplifier")
effect.applyEffect()
}
}
class SolidStateAmplifier(effect: Effect) : Amplifier(effect) {
override fun play() {
println("Playing through solid state amplifier")
effect.applyEffect()
}
}
// Client
fun main() {
val reverb = ReverbEffect()
val distortion = DistortionEffect()
val tubeAmp = TubeAmplifier(reverb)
val solidStateAmp = SolidStateAmplifier(distortion)
tubeAmp.play()
solidStateAmp.play()
}
// The Implementation interface
interface Effect {
applyEffect(): void;
}
// The Concrete implementations
class ReverbEffect implements Effect {
applyEffect(): void {
console.log("Applying reverb effect");
}
}
class DistortionEffect implements Effect {
applyEffect(): void {
console.log("Applying distortion effect");
}
}
// The Abstraction
abstract class Amplifier {
protected effect: Effect;
constructor(effect: Effect) {
this.effect = effect;
}
abstract play(): void;
}
// The Refined abstraction
class TubeAmplifier extends Amplifier {
constructor(effect: Effect) {
super(effect);
}
play(): void {
console.log("Playing through tube amplifier");
this.effect.applyEffect();
}
}
class SolidStateAmplifier extends Amplifier {
constructor(effect: Effect) {
super(effect);
}
play(): void {
console.log("Playing through solid state amplifier");
this.effect.applyEffect();
}
}
// Client
const reverb = new ReverbEffect();
const distortion = new DistortionEffect();
const tubeAmp = new TubeAmplifier(reverb);
const solidStateAmp = new SolidStateAmplifier(distortion);
tubeAmp.play();
solidStateAmp.play();
// The Implementation interface
abstract class Effect {
void applyEffect();
}
// The Concrete implementations
class ReverbEffect implements Effect {
@override
void applyEffect() {
print("Applying reverb effect");
}
}
class DistortionEffect implements Effect {
@override
void applyEffect() {
print("Applying distortion effect");
}
}
// The Abstraction
abstract class Amplifier {
final Effect effect;
Amplifier(this.effect);
void play();
}
// The Refined abstraction
class TubeAmplifier extends Amplifier {
TubeAmplifier(Effect effect) : super(effect);
@override
void play() {
print("Playing through tube amplifier");
effect.applyEffect();
}
}
class SolidStateAmplifier extends Amplifier {
SolidStateAmplifier(Effect effect) : super(effect);
@override
void play() {
print("Playing through solid state amplifier");
effect.applyEffect();
}
}
// Client
void main() {
final reverb = ReverbEffect();
final distortion = DistortionEffect();
final tubeAmp = TubeAmplifier(reverb);
final solidStateAmp = SolidStateAmplifier(distortion);
tubeAmp.play();
solidStateAmp.play();
}
// The Implementation protocol
protocol Effect {
func applyEffect()
}
// The Concrete implementations
class ReverbEffect: Effect {
func applyEffect() {
print("Applying reverb effect")
}
}
class DistortionEffect: Effect {
func applyEffect() {
print("Applying distortion effect")
}
}
// The Abstraction
class Amplifier {
let effect: Effect
init(effect: Effect) {
self.effect = effect
}
func play() {
fatalError("play() must be overridden")
}
}
// The Refined abstraction
class TubeAmplifier: Amplifier {
override func play() {
print("Playing through tube amplifier")
effect.applyEffect()
}
}
class SolidStateAmplifier: Amplifier {
override func play() {
print("Playing through solid state amplifier")
effect.applyEffect()
}
}
// Client
let reverb = ReverbEffect()
let distortion = DistortionEffect()
let tubeAmp = TubeAmplifier(effect: reverb)
let solidStateAmp = SolidStateAmplifier(effect: distortion)
tubeAmp.play()
solidStateAmp.play()
from abc import ABC, abstractmethod
# The Implementation interface
class Effect(ABC):
@abstractmethod
def apply_effect(self):
pass
# The Concrete implementations
class ReverbEffect(Effect):
def apply_effect(self):
print("Applying reverb effect")
class DistortionEffect(Effect):
def apply_effect(self):
print("Applying distortion effect")
# The Abstraction
class Amplifier(ABC):
def __init__(self, effect: Effect):
self.effect = effect
@abstractmethod
def play(self):
pass
# The Refined abstraction
class TubeAmplifier(Amplifier):
def play(self):
print("Playing through tube amplifier")
self.effect.apply_effect()
class SolidStateAmplifier(Amplifier):
def play(self):
print("Playing through solid state amplifier")
self.effect.apply_effect()
# Client
reverb = ReverbEffect()
distortion = DistortionEffect()
tube_amp = TubeAmplifier(reverb)
solid_state_amp = SolidStateAmplifier(distortion)
tube_amp.play()
solid_state_amp.play()
Summary
The Bridge design pattern separates an abstraction from its implementation, allowing both to vary independently. This pattern is particularly useful when you want to avoid a permanent binding between an abstraction and its implementation, or when both the abstraction and its implementation need to be extended by subclassing.
The Composite design pattern helps you compose objects into tree structures to represent part-whole hierarchies. Instead of treating individual objects and groups of objects differently, you define a common interface that both implement. This keeps the client code simple since it can treat leaves and composites uniformly. As a result, you can build complex tree structures where branches and leaves share the same operations.
Key Concepts
- Component: The base interface for all objects in the composition, declaring common operations for both leaves and composites.
- Leaf: The end object of a composition that has no children.
- Composite: The container that holds child components and implements operations by delegating to its children.
- Client: The class that manipulates objects in the composition through the Component interface.
Benefits
- It simplifies client code by allowing uniform treatment of individual objects and compositions.
- It makes it easier to add new kinds of components without changing existing code.
Let's make the Composite pattern more concrete with a practical example. Imagine we have a guitar effects system where individual effects (reverb, delay) can be combined into effects chains. By using the Composite, we can treat a single effect and a chain of effects the same way.
The diagram below illustrates how the Composite pattern is applied in our effects scenario: both individual effects (Reverb, Delay) and the EffectsChain implement the same Effect interface, allowing nested structures:
classDiagram
class Effect {
<<interface>>
+applyEffect()
}
class Reverb {
+applyEffect()
}
class Delay {
+applyEffect()
}
class EffectsChain {
-effects: List~Effect~
+addEffect(Effect)
+removeEffect(Effect)
+applyEffect()
}
Effect <|.. Reverb
Effect <|.. Delay
Effect <|.. EffectsChain
EffectsChain o-- Effect
Now, let's see how this pattern comes to life in code by implementing a guitar effects chain that can contain individual effects or nested chains.
// The Component
interface Effect {
void applyEffect();
}
// The Leaf
class Reverb implements Effect {
@Override
public void applyEffect() {
System.out.println("Applying reverb");
}
}
// The Leaf
class Delay implements Effect {
@Override
public void applyEffect() {
System.out.println("Applying delay");
}
}
// The Composite
class EffectsChain implements Effect {
private List<Effect> effects = new ArrayList<>();
public void addEffect(Effect effect) {
effects.add(effect);
}
public void removeEffect(Effect effect) {
effects.remove(effect);
}
@Override
public void applyEffect() {
for (Effect effect : effects) {
effect.applyEffect();
}
}
}
// Client
public class Main {
public static void main(String[] args) {
Reverb reverb = new Reverb();
Delay delay = new Delay();
EffectsChain chain1 = new EffectsChain();
chain1.addEffect(reverb);
chain1.addEffect(delay);
Reverb reverb2 = new Reverb();
EffectsChain chain2 = new EffectsChain();
chain2.addEffect(reverb2);
chain2.addEffect(chain1);
chain2.applyEffect();
}
}
// The Component
interface Effect {
fun applyEffect()
}
// The Leaf
class Reverb : Effect {
override fun applyEffect() {
println("Applying reverb")
}
}
// The Leaf
class Delay : Effect {
override fun applyEffect() {
println("Applying delay")
}
}
// The Composite
class EffectsChain : Effect {
private val effects = mutableListOf<Effect>()
fun addEffect(effect: Effect) {
effects.add(effect)
}
fun removeEffect(effect: Effect) {
effects.remove(effect)
}
override fun applyEffect() {
effects.forEach { it.applyEffect() }
}
}
// Client
fun main() {
val reverb = Reverb()
val delay = Delay()
val chain1 = EffectsChain()
chain1.addEffect(reverb)
chain1.addEffect(delay)
val reverb2 = Reverb()
val chain2 = EffectsChain()
chain2.addEffect(reverb2)
chain2.addEffect(chain1)
chain2.applyEffect()
}
// The Component
interface Effect {
applyEffect(): void;
}
// The Leaf
class Reverb implements Effect {
applyEffect(): void {
console.log("Applying reverb");
}
}
// The Leaf
class Delay implements Effect {
applyEffect(): void {
console.log("Applying delay");
}
}
// The Composite
class EffectsChain implements Effect {
private effects: Effect[] = [];
addEffect(effect: Effect): void {
this.effects.push(effect);
}
removeEffect(effect: Effect): void {
this.effects = this.effects.filter(e => e !== effect);
}
applyEffect(): void {
this.effects.forEach(effect => effect.applyEffect());
}
}
// Client
const reverb = new Reverb();
const delay = new Delay();
const chain1 = new EffectsChain();
chain1.addEffect(reverb);
chain1.addEffect(delay);
const reverb2 = new Reverb();
const chain2 = new EffectsChain();
chain2.addEffect(reverb2);
chain2.addEffect(chain1);
chain2.applyEffect();
// The Component
abstract class Effect {
void applyEffect();
}
// The Leaf
class Reverb implements Effect {
@override
void applyEffect() {
print("Applying reverb");
}
}
// The Leaf
class Delay implements Effect {
@override
void applyEffect() {
print("Applying delay");
}
}
// The Composite
class EffectsChain implements Effect {
List<Effect> effects = [];
void addEffect(Effect effect) {
effects.add(effect);
}
void removeEffect(Effect effect) {
effects.remove(effect);
}
@override
void applyEffect() {
effects.forEach((effect) => effect.applyEffect());
}
}
// Client
void main() {
Reverb reverb = Reverb();
Delay delay = Delay();
EffectsChain chain1 = EffectsChain();
chain1.addEffect(reverb);
chain1.addEffect(delay);
Reverb reverb2 = Reverb();
EffectsChain chain2 = EffectsChain();
chain2.addEffect(reverb2);
chain2.addEffect(chain1);
chain2.applyEffect();
}
// The Component
protocol Effect {
func applyEffect()
}
// The Leaf
class Reverb: Effect {
func applyEffect() {
print("Applying reverb")
}
}
// The Leaf
class Delay: Effect {
func applyEffect() {
print("Applying delay")
}
}
// The Composite
class EffectsChain: Effect {
private var effects: [Effect] = []
func addEffect(_ effect: Effect) {
effects.append(effect)
}
func removeEffect(_ effect: Effect) {
effects.removeAll { $0 === effect }
}
func applyEffect() {
effects.forEach { $0.applyEffect() }
}
}
// Client
let reverb = Reverb()
let delay = Delay()
let chain1 = EffectsChain()
chain1.addEffect(reverb)
chain1.addEffect(delay)
let reverb2 = Reverb()
let chain2 = EffectsChain()
chain2.addEffect(reverb2)
chain2.addEffect(chain1)
chain2.applyEffect()
from abc import ABC, abstractmethod
# The Component
class Effect(ABC):
@abstractmethod
def apply_effect(self):
pass
# The Leaf
class Reverb(Effect):
def apply_effect(self):
print("Applying reverb")
# The Leaf
class Delay(Effect):
def apply_effect(self):
print("Applying delay")
# The Composite
class EffectsChain(Effect):
def __init__(self):
self.effects = []
def add_effect(self, effect):
self.effects.append(effect)
def remove_effect(self, effect):
self.effects.remove(effect)
def apply_effect(self):
for effect in self.effects:
effect.apply_effect()
# Client
reverb = Reverb()
delay = Delay()
chain1 = EffectsChain()
chain1.add_effect(reverb)
chain1.add_effect(delay)
reverb2 = Reverb()
chain2 = EffectsChain()
chain2.add_effect(reverb2)
chain2.add_effect(chain1)
chain2.apply_effect()
Summary
The Composite pattern provides a way to treat a group of objects as a single object. It simplifies client code by allowing it to interact with individual components and compositions of components in a uniform manner. This pattern is useful when you have a hierarchical structure of objects and you want to perform operations on the entire structure or parts of it.
The Decorator design pattern helps you attach additional behaviors to objects dynamically without affecting other objects of the same class. Instead of using inheritance to extend functionality, you wrap the original object with decorator objects that add new behaviors. This keeps the original class unchanged while allowing flexible combinations of behaviors. As a result, you can stack multiple decorators to compose complex functionality at runtime.
Key Concepts
- Component: The interface defining the object to which additional responsibilities can be attached.
- Concrete Component: The base object to which decorators can add behavior.
- Decorator: The abstract class that implements the Component interface and wraps a Component object.
- Concrete Decorators: The classes that extend the Decorator and add specific behaviors.
Benefits
- It allows adding new behaviors without modifying existing classes (Open/Closed Principle).
- It enables flexible combinations of behaviors that can be added or removed at runtime.
Let's make the Decorator pattern more concrete with a practical example. Imagine we have a guitar amplifier that we want to enhance with various effects like reverb and chorus. By using the Decorator, we can wrap the amplifier with effect decorators and stack them in any combination.
The diagram below illustrates how the Decorator pattern is applied in our amplifier scenario: decorators like ReverbDecorator and ChorusDecorator wrap the base CleanAmplifier, each adding their own effect while maintaining the same interface:
classDiagram
class Amplifier {
<<interface>>
+play() String
}
class CleanAmplifier {
+play() String
}
class AmplifierDecorator {
<<abstract>>
#amplifier: Amplifier
+play() String
}
class ReverbDecorator {
+play() String
}
class ChorusDecorator {
+play() String
}
Amplifier <|.. CleanAmplifier
Amplifier <|.. AmplifierDecorator
AmplifierDecorator o-- Amplifier
AmplifierDecorator <|-- ReverbDecorator
AmplifierDecorator <|-- ChorusDecorator
Now, let's see how this pattern comes to life in code by implementing an amplifier that can be dynamically enhanced with effect decorators.
// The Component
interface Amplifier {
String play();
}
// The Concrete Component
class CleanAmplifier implements Amplifier {
@Override
public String play() {
return "Clean amplifier sound";
}
}
// The Decorator
abstract class AmplifierDecorator implements Amplifier {
protected Amplifier amplifier;
public AmplifierDecorator(Amplifier amplifier) {
this.amplifier = amplifier;
}
@Override
public String play() {
return amplifier.play();
}
}
// The Concrete Decorator
class ReverbDecorator extends AmplifierDecorator {
public ReverbDecorator(Amplifier amplifier) {
super(amplifier);
}
@Override
public String play() {
return super.play() + " with reverb";
}
}
// The Concrete Decorator
class ChorusDecorator extends AmplifierDecorator {
public ChorusDecorator(Amplifier amplifier) {
super(amplifier);
}
@Override
public String play() {
return super.play() + " with chorus";
}
}
// Client
public class Main {
public static void main(String[] args) {
Amplifier cleanAmp = new CleanAmplifier();
System.out.println(cleanAmp.play()); // Output: Clean amplifier sound
Amplifier reverbAmp = new ReverbDecorator(cleanAmp);
System.out.println(reverbAmp.play()); // Output: Clean amplifier sound with reverb
Amplifier chorusReverbAmp = new ChorusDecorator(reverbAmp);
System.out.println(chorusReverbAmp.play()); // Output: Clean amplifier sound with reverb with chorus
}
}
// The Component
interface Amplifier {
fun play(): String
}
// The Concrete Component
class CleanAmplifier : Amplifier {
override fun play(): String {
return "Clean amplifier sound"
}
}
// The Decorator
abstract class AmplifierDecorator(private val amplifier: Amplifier) : Amplifier {
override fun play(): String {
return amplifier.play()
}
}
// The Concrete Decorator
class ReverbDecorator(amplifier: Amplifier) : AmplifierDecorator(amplifier) {
override fun play(): String {
return super.play() + " with reverb"
}
}
// The Concrete Decorator
class ChorusDecorator(amplifier: Amplifier) : AmplifierDecorator(amplifier) {
override fun play(): String {
return super.play() + " with chorus"
}
}
// Client
fun main() {
val cleanAmp: Amplifier = CleanAmplifier()
println(cleanAmp.play()) // Output: Clean amplifier sound
val reverbAmp: Amplifier = ReverbDecorator(cleanAmp)
println(reverbAmp.play()) // Output: Clean amplifier sound with reverb
val chorusReverbAmp: Amplifier = ChorusDecorator(reverbAmp)
println(chorusReverbAmp.play()) // Output: Clean amplifier sound with reverb with chorus
}
// The Component
interface Amplifier {
play(): string;
}
// The Concrete Component
class CleanAmplifier implements Amplifier {
play(): string {
return "Clean amplifier sound";
}
}
// The Decorator
abstract class AmplifierDecorator implements Amplifier {
protected amplifier: Amplifier;
constructor(amplifier: Amplifier) {
this.amplifier = amplifier;
}
play(): string {
return this.amplifier.play();
}
}
// The Concrete Decorator
class ReverbDecorator extends AmplifierDecorator {
constructor(amplifier: Amplifier) {
super(amplifier);
}
play(): string {
return super.play() + " with reverb";
}
}
// The Concrete Decorator
class ChorusDecorator extends AmplifierDecorator {
constructor(amplifier: Amplifier) {
super(amplifier);
}
play(): string {
return super.play() + " with chorus";
}
}
// Client
function main() {
const cleanAmp: Amplifier = new CleanAmplifier();
console.log(cleanAmp.play()); // Output: Clean amplifier sound
const reverbAmp: Amplifier = new ReverbDecorator(cleanAmp);
console.log(reverbAmp.play()); // Output: Clean amplifier sound with reverb
const chorusReverbAmp: Amplifier = new ChorusDecorator(reverbAmp);
console.log(chorusReverbAmp.play()); // Output: Clean amplifier sound with reverb with chorus
}
main();
// The Component
abstract class Amplifier {
String play();
}
// The Concrete Component
class CleanAmplifier implements Amplifier {
@override
String play() {
return "Clean amplifier sound";
}
}
// The Decorator
abstract class AmplifierDecorator implements Amplifier {
final Amplifier amplifier;
AmplifierDecorator(this.amplifier);
@override
String play() {
return amplifier.play();
}
}
// The Concrete Decorator
class ReverbDecorator extends AmplifierDecorator {
ReverbDecorator(Amplifier amplifier) : super(amplifier);
@override
String play() {
return super.play() + " with reverb";
}
}
// The Concrete Decorator
class ChorusDecorator extends AmplifierDecorator {
ChorusDecorator(Amplifier amplifier) : super(amplifier);
@override
String play() {
return super.play() + " with chorus";
}
}
// Client
void main() {
Amplifier cleanAmp = CleanAmplifier();
print(cleanAmp.play()); // Output: Clean amplifier sound
Amplifier reverbAmp = ReverbDecorator(cleanAmp);
print(reverbAmp.play()); // Output: Clean amplifier sound with reverb
Amplifier chorusReverbAmp = ChorusDecorator(reverbAmp);
print(chorusReverbAmp.play()); // Output: Clean amplifier sound with reverb with chorus
}
// The Component
protocol Amplifier {
func play() -> String
}
// The Concrete Component
class CleanAmplifier: Amplifier {
func play() -> String {
return "Clean amplifier sound"
}
}
// The Decorator
class AmplifierDecorator: Amplifier {
private let amplifier: Amplifier
init(amplifier: Amplifier) {
self.amplifier = amplifier
}
func play() -> String {
return amplifier.play()
}
}
// The Concrete Decorator
class ReverbDecorator: AmplifierDecorator {
override func play() -> String {
return super.play() + " with reverb"
}
}
// The Concrete Decorator
class ChorusDecorator: AmplifierDecorator {
override func play() -> String {
return super.play() + " with chorus"
}
}
// Client
func main() {
let cleanAmp: Amplifier = CleanAmplifier()
print(cleanAmp.play()) // Output: Clean amplifier sound
let reverbAmp: Amplifier = ReverbDecorator(amplifier: cleanAmp)
print(reverbAmp.play()) // Output: Clean amplifier sound with reverb
let chorusReverbAmp: Amplifier = ChorusDecorator(amplifier: reverbAmp)
print(chorusReverbAmp.play()) // Output: Clean amplifier sound with reverb with chorus
}
main()
# The Component
class Amplifier:
def play(self):
pass
# The Concrete Component
class CleanAmplifier(Amplifier):
def play(self):
return "Clean amplifier sound"
# The Decorator
class AmplifierDecorator(Amplifier):
def __init__(self, amplifier):
self.amplifier = amplifier
def play(self):
return self.amplifier.play()
# The Concrete Decorator
class ReverbDecorator(AmplifierDecorator):
def play(self):
return super().play() + " with reverb"
# The Concrete Decorator
class ChorusDecorator(AmplifierDecorator):
def play(self):
return super().play() + " with chorus"
# Client
clean_amp = CleanAmplifier()
print(clean_amp.play()) # Output: Clean amplifier sound
reverb_amp = ReverbDecorator(clean_amp)
print(reverb_amp.play()) # Output: Clean amplifier sound with reverb
chorus_reverb_amp = ChorusDecorator(reverb_amp)
print(chorus_reverb_amp.play()) # Output: Clean amplifier sound with reverb with chorus
Summary
The Decorator pattern allows you to add responsibilities to objects dynamically. It provides a flexible alternative to subclassing for extending functionality, as you can add or remove decorators at runtime to compose different combinations of behaviors. This pattern is particularly useful when you need to support multiple combinations of optional features and want to avoid class explosion.
The Facade design pattern helps you provide a simplified interface to a complex system of classes, libraries, or frameworks. Instead of exposing all the intricate details of multiple subsystems, you create a facade that orchestrates the underlying components. This keeps client code simple and decoupled from the subsystem internals. As a result, you can use complex systems through a single, easy-to-understand entry point.
Key Concepts
- Facade: The class that provides a simple interface to the complex subsystem and knows which classes handle each request.
- Subsystem: The classes that implement the actual functionality and handle the work.
- Client: The class that interacts with the subsystem through the Facade.
Benefits
- It simplifies the interface of a complex system for client code.
- It decouples the client from the subsystem components, promoting loose coupling.
Let's make the Facade pattern more concrete with a practical example. Imagine we have a guitar recording system with multiple complex components: microphone preamps, audio interfaces, and digital audio workstations. By using the Facade, we can hide this complexity behind a simple recording interface.
The diagram below illustrates how the Facade pattern is applied in our recording scenario: the GuitarRecordingFacade orchestrates the MicrophonePreamp, AudioInterface, and DAW subsystems, providing a single method to record a guitar track:
classDiagram
class GuitarRecordingFacade {
-preamp: MicrophonePreamp
-audioInterface: AudioInterface
-daw: DAW
+recordGuitarTrack()
}
class MicrophonePreamp {
+amplifySignal()
}
class AudioInterface {
+convertAnalogToDigital()
}
class DAW {
+recordAudio()
}
GuitarRecordingFacade o-- MicrophonePreamp
GuitarRecordingFacade o-- AudioInterface
GuitarRecordingFacade o-- DAW
Now, let's see how this pattern comes to life in code by implementing a recording facade that simplifies the complex guitar recording workflow.
// The Subsystem classes
class MicrophonePreamp {
public void amplifySignal() {
System.out.println("Amplifying microphone signal");
}
}
class AudioInterface {
public void convertAnalogToDigital() {
System.out.println("Converting analog signal to digital");
}
}
class DAW {
public void recordAudio() {
System.out.println("Recording audio in DAW");
}
}
// The Facade
class GuitarRecordingFacade {
private MicrophonePreamp preamp;
private AudioInterface audioInterface;
private DAW daw;
public GuitarRecordingFacade() {
this.preamp = new MicrophonePreamp();
this.audioInterface = new AudioInterface();
this.daw = new DAW();
}
public void recordGuitarTrack() {
preamp.amplifySignal();
audioInterface.convertAnalogToDigital();
daw.recordAudio();
System.out.println("Guitar track recorded successfully!");
}
}
// Client
public class Main {
public static void main(String[] args) {
GuitarRecordingFacade recordingFacade = new GuitarRecordingFacade();
recordingFacade.recordGuitarTrack();
}
}
// The Subsystem classes
class MicrophonePreamp {
fun amplifySignal() {
println("Amplifying microphone signal")
}
}
class AudioInterface {
fun convertAnalogToDigital() {
println("Converting analog signal to digital")
}
}
class DAW {
fun recordAudio() {
println("Recording audio in DAW")
}
}
// The Facade
class GuitarRecordingFacade {
private val preamp = MicrophonePreamp()
private val audioInterface = AudioInterface()
private val daw = DAW()
fun recordGuitarTrack() {
preamp.amplifySignal()
audioInterface.convertAnalogToDigital()
daw.recordAudio()
println("Guitar track recorded successfully!")
}
}
// Client
fun main() {
val recordingFacade = GuitarRecordingFacade()
recordingFacade.recordGuitarTrack()
}
// The Subsystem classes
class MicrophonePreamp {
amplifySignal() {
console.log("Amplifying microphone signal");
}
}
class AudioInterface {
convertAnalogToDigital() {
console.log("Converting analog signal to digital");
}
}
class DAW {
recordAudio() {
console.log("Recording audio in DAW");
}
}
// The Facade
class GuitarRecordingFacade {
private preamp: MicrophonePreamp;
private audioInterface: AudioInterface;
private daw: DAW;
constructor() {
this.preamp = new MicrophonePreamp();
this.audioInterface = new AudioInterface();
this.daw = new DAW();
}
recordGuitarTrack() {
this.preamp.amplifySignal();
this.audioInterface.convertAnalogToDigital();
this.daw.recordAudio();
console.log("Guitar track recorded successfully!");
}
}
// Client
const recordingFacade = new GuitarRecordingFacade();
recordingFacade.recordGuitarTrack();
// The Subsystem classes
class MicrophonePreamp {
void amplifySignal() {
print("Amplifying microphone signal");
}
}
class AudioInterface {
void convertAnalogToDigital() {
print("Converting analog signal to digital");
}
}
class DAW {
void recordAudio() {
print("Recording audio in DAW");
}
}
// The Facade
class GuitarRecordingFacade {
final MicrophonePreamp preamp = MicrophonePreamp();
final AudioInterface audioInterface = AudioInterface();
final DAW daw = DAW();
void recordGuitarTrack() {
preamp.amplifySignal();
audioInterface.convertAnalogToDigital();
daw.recordAudio();
print("Guitar track recorded successfully!");
}
}
// Client
void main() {
final recordingFacade = GuitarRecordingFacade();
recordingFacade.recordGuitarTrack();
}
// The Subsystem classes
class MicrophonePreamp {
func amplifySignal() {
print("Amplifying microphone signal")
}
}
class AudioInterface {
func convertAnalogToDigital() {
print("Converting analog signal to digital")
}
}
class DAW {
func recordAudio() {
print("Recording audio in DAW")
}
}
// The Facade
class GuitarRecordingFacade {
private let preamp = MicrophonePreamp()
private let audioInterface = AudioInterface()
private let daw = DAW()
func recordGuitarTrack() {
preamp.amplifySignal()
audioInterface.convertAnalogToDigital()
daw.recordAudio()
print("Guitar track recorded successfully!")
}
}
// Client
let recordingFacade = GuitarRecordingFacade()
recordingFacade.recordGuitarTrack()
# The Subsystem classes
class MicrophonePreamp:
def amplify_signal(self):
print("Amplifying microphone signal")
class AudioInterface:
def convert_analog_to_digital(self):
print("Converting analog signal to digital")
class DAW:
def record_audio(self):
print("Recording audio in DAW")
# The Facade
class GuitarRecordingFacade:
def __init__(self):
self.preamp = MicrophonePreamp()
self.audio_interface = AudioInterface()
self.daw = DAW()
def record_guitar_track(self):
self.preamp.amplify_signal()
self.audio_interface.convert_analog_to_digital()
self.daw.record_audio()
print("Guitar track recorded successfully!")
# Client
recording_facade = GuitarRecordingFacade()
recording_facade.record_guitar_track()
Summary
The Facade pattern simplifies the interface to a complex subsystem, providing a higher-level interface that makes the subsystem easier to use. It promotes loose coupling between the client and the subsystem components, allowing you to change the subsystem without affecting the client code. This pattern is particularly useful when you want to provide a simple and easy-to-use interface to a complex system, hide the complexities of the system from the client, and reduce dependencies between the client and the subsystem.
The Flyweight design pattern helps you minimize memory usage by sharing as much data as possible with similar objects. Instead of creating a new object for every instance, you separate intrinsic state (shared, immutable) from extrinsic state (unique, passed in). This keeps memory consumption low when dealing with large numbers of similar objects. As a result, you can support massive numbers of fine-grained objects efficiently.
Key Concepts
- Flyweight: The interface through which flyweights receive and act on extrinsic state.
- Concrete Flyweight: The class that implements the Flyweight interface and stores intrinsic (shared) state.
- Flyweight Factory: The class that creates and manages flyweight objects, ensuring they are shared properly.
- Client: The class that maintains references to flyweights and computes or stores extrinsic state.
Benefits
- It reduces memory usage by sharing objects with common intrinsic state.
- It improves performance by reducing the number of objects created.
Let's make the Flyweight pattern more concrete with a practical example. Imagine we have a guitar effects system where many pedals share the same effect type but have different user settings. By using the Flyweight, we can share the effect type (intrinsic state) while passing in settings (extrinsic state) at runtime.
The diagram below illustrates how the Flyweight pattern is applied in our effects scenario: the GuitarEffectFactory manages shared effect instances (ChorusEffect, DelayEffect) while GuitarSettings represents the varying extrinsic state:
classDiagram
class GuitarEffect {
<<interface>>
+applyEffect(GuitarSettings)
}
class ChorusEffect {
-effectType: String
+applyEffect(GuitarSettings)
}
class DelayEffect {
-effectType: String
+applyEffect(GuitarSettings)
}
class GuitarEffectFactory {
-effects: Map~String, GuitarEffect~$
+getEffect(String)$ GuitarEffect
}
class GuitarSettings {
-intensity: int
-speed: int
+toString() String
}
GuitarEffect <|.. ChorusEffect
GuitarEffect <|.. DelayEffect
GuitarEffectFactory ..> GuitarEffect : manages
GuitarEffect ..> GuitarSettings : uses
Now, let's see how this pattern comes to life in code by implementing a flyweight factory that shares effect objects across multiple usages.
// The Flyweight interface
interface GuitarEffect {
void applyEffect(GuitarSettings settings);
}
// The Concrete Flyweight
class ChorusEffect implements GuitarEffect {
private String effectType = "Chorus";
@Override
public void applyEffect(GuitarSettings settings) {
System.out.println("Applying " + effectType + " effect with settings: " + settings.toString());
}
}
// The Concrete Flyweight
class DelayEffect implements GuitarEffect {
private String effectType = "Delay";
@Override
public void applyEffect(GuitarSettings settings) {
System.out.println("Applying " + effectType + " effect with settings: " + settings.toString());
}
}
// The Flyweight Factory
class GuitarEffectFactory {
private static final Map<String, GuitarEffect> effects = new HashMap<>();
public static GuitarEffect getEffect(String effectType) {
GuitarEffect effect = effects.get(effectType);
if (effect == null) {
switch (effectType) {
case "Chorus":
effect = new ChorusEffect();
break;
case "Delay":
effect = new DelayEffect();
break;
default:
throw new IllegalArgumentException("Effect type not supported");
}
effects.put(effectType, effect);
}
return effect;
}
}
// The Extrinsic state
class GuitarSettings {
private int intensity;
private int speed;
public GuitarSettings(int intensity, int speed) {
this.intensity = intensity;
this.speed = speed;
}
@Override
public String toString() {
return "Intensity: " + intensity + ", Speed: " + speed;
}
}
// Client
public class Main {
public static void main(String[] args) {
GuitarEffect chorus = GuitarEffectFactory.getEffect("Chorus");
GuitarEffect delay = GuitarEffectFactory.getEffect("Delay");
GuitarSettings settings1 = new GuitarSettings(5, 10);
GuitarSettings settings2 = new GuitarSettings(7, 12);
chorus.applyEffect(settings1);
delay.applyEffect(settings2);
chorus.applyEffect(settings2);
}
}
// The Flyweight interface
interface GuitarEffect {
fun applyEffect(settings: GuitarSettings)
}
// The Concrete Flyweight
class ChorusEffect : GuitarEffect {
private val effectType = "Chorus"
override fun applyEffect(settings: GuitarSettings) {
println("Applying $effectType effect with settings: $settings")
}
}
// The Concrete Flyweight
class DelayEffect : GuitarEffect {
private val effectType = "Delay"
override fun applyEffect(settings: GuitarSettings) {
println("Applying $effectType effect with settings: $settings")
}
}
// The Flyweight Factory
object GuitarEffectFactory {
private val effects = mutableMapOf<String, GuitarEffect>()
fun getEffect(effectType: String): GuitarEffect {
return effects.getOrPut(effectType) {
when (effectType) {
"Chorus" -> ChorusEffect()
"Delay" -> DelayEffect()
else -> throw IllegalArgumentException("Effect type not supported")
}
}
}
}
// The Extrinsic state
data class GuitarSettings(val intensity: Int, val speed: Int)
// Client
fun main() {
val chorus = GuitarEffectFactory.getEffect("Chorus")
val delay = GuitarEffectFactory.getEffect("Delay")
val settings1 = GuitarSettings(5, 10)
val settings2 = GuitarSettings(7, 12)
chorus.applyEffect(settings1)
delay.applyEffect(settings2)
chorus.applyEffect(settings2)
}
// The Flyweight interface
interface GuitarEffect {
applyEffect(settings: GuitarSettings): void;
}
// The Concrete Flyweight
class ChorusEffect implements GuitarEffect {
private effectType: string = "Chorus";
applyEffect(settings: GuitarSettings): void {
console.log(`Applying ${this.effectType} effect with settings: ${settings.toString()}`);
}
}
// The Concrete Flyweight
class DelayEffect implements GuitarEffect {
private effectType: string = "Delay";
applyEffect(settings: GuitarSettings): void {
console.log(`Applying ${this.effectType} effect with settings: ${settings.toString()}`);
}
}
// The Flyweight Factory
class GuitarEffectFactory {
private static effects: { [key: string]: GuitarEffect } = {};
static getEffect(effectType: string): GuitarEffect {
if (!GuitarEffectFactory.effects[effectType]) {
switch (effectType) {
case "Chorus":
GuitarEffectFactory.effects[effectType] = new ChorusEffect();
break;
case "Delay":
GuitarEffectFactory.effects[effectType] = new DelayEffect();
break;
default:
throw new Error("Effect type not supported");
}
}
return GuitarEffectFactory.effects[effectType];
}
}
// The Extrinsic state
class GuitarSettings {
private intensity: number;
private speed: number;
constructor(intensity: number, speed: number) {
this.intensity = intensity;
this.speed = speed;
}
toString(): string {
return `Intensity: ${this.intensity}, Speed: ${this.speed}`;
}
}
// Client
function main() {
const chorus = GuitarEffectFactory.getEffect("Chorus");
const delay = GuitarEffectFactory.getEffect("Delay");
const settings1 = new GuitarSettings(5, 10);
const settings2 = new GuitarSettings(7, 12);
chorus.applyEffect(settings1);
delay.applyEffect(settings2);
chorus.applyEffect(settings2);
}
main();
// The Flyweight interface
abstract class GuitarEffect {
void applyEffect(GuitarSettings settings);
}
// The Concrete Flyweight
class ChorusEffect implements GuitarEffect {
final String effectType = "Chorus";
@override
void applyEffect(GuitarSettings settings) {
print("Applying $effectType effect with settings: ${settings.toString()}");
}
}
// The Concrete Flyweight
class DelayEffect implements GuitarEffect {
final String effectType = "Delay";
@override
void applyEffect(GuitarSettings settings) {
print("Applying $effectType effect with settings: ${settings.toString()}");
}
}
// The Flyweight Factory
class GuitarEffectFactory {
static final Map<String, GuitarEffect> _effects = {};
static GuitarEffect getEffect(String effectType) {
if (!_effects.containsKey(effectType)) {
switch (effectType) {
case "Chorus":
_effects[effectType] = ChorusEffect();
break;
case "Delay":
_effects[effectType] = DelayEffect();
break;
default:
throw ArgumentError("Effect type not supported");
}
}
return _effects[effectType]!;
}
}
// The Extrinsic state
class GuitarSettings {
final int intensity;
final int speed;
GuitarSettings(this.intensity, this.speed);
@override
String toString() {
return "Intensity: $intensity, Speed: $speed";
}
}
// Client
void main() {
final chorus = GuitarEffectFactory.getEffect("Chorus");
final delay = GuitarEffectFactory.getEffect("Delay");
final settings1 = GuitarSettings(5, 10);
final settings2 = GuitarSettings(7, 12);
chorus.applyEffect(settings1);
delay.applyEffect(settings2);
chorus.applyEffect(settings2);
}
// The Flyweight interface
protocol GuitarEffect {
func applyEffect(settings: GuitarSettings)
}
// The Concrete Flyweight
class ChorusEffect: GuitarEffect {
private let effectType = "Chorus"
func applyEffect(settings: GuitarSettings) {
print("Applying \(effectType) effect with settings: \(settings.toString())")
}
}
// The Concrete Flyweight
class DelayEffect: GuitarEffect {
private let effectType = "Delay"
func applyEffect(settings: GuitarSettings) {
print("Applying \(effectType) effect with settings: \(settings.toString())")
}
}
// The Flyweight Factory
class GuitarEffectFactory {
private static var effects: [String: GuitarEffect] = [:]
static func getEffect(effectType: String) -> GuitarEffect {
if let effect = effects[effectType] {
return effect
} else {
var newEffect: GuitarEffect
switch effectType {
case "Chorus":
newEffect = ChorusEffect()
case "Delay":
newEffect = DelayEffect()
default:
fatalError("Effect type not supported")
}
effects[effectType] = newEffect
return newEffect
}
}
}
// The Extrinsic state
class GuitarSettings {
let intensity: Int
let speed: Int
init(intensity: Int, speed: Int) {
self.intensity = intensity
self.speed = speed
}
func toString() -> String {
return "Intensity: \(intensity), Speed: \(speed)"
}
}
// Client
func main() {
let chorus = GuitarEffectFactory.getEffect(effectType: "Chorus")
let delay = GuitarEffectFactory.getEffect(effectType: "Delay")
let settings1 = GuitarSettings(intensity: 5, speed: 10)
let settings2 = GuitarSettings(intensity: 7, speed: 12)
chorus.applyEffect(settings: settings1)
delay.applyEffect(settings: settings2)
chorus.applyEffect(settings: settings2)
}
main()
# The Flyweight interface
class GuitarEffect:
def apply_effect(self, settings):
pass
# The Concrete Flyweight
class ChorusEffect(GuitarEffect):
def __init__(self):
self.effect_type = "Chorus"
def apply_effect(self, settings):
print(f"Applying {self.effect_type} effect with settings: {settings}")
# The Concrete Flyweight
class DelayEffect(GuitarEffect):
def __init__(self):
self.effect_type = "Delay"
def apply_effect(self, settings):
print(f"Applying {self.effect_type} effect with settings: {settings}")
# The Flyweight Factory
class GuitarEffectFactory:
_effects = {}
@staticmethod
def get_effect(effect_type):
if effect_type not in GuitarEffectFactory._effects:
if effect_type == "Chorus":
GuitarEffectFactory._effects[effect_type] = ChorusEffect()
elif effect_type == "Delay":
GuitarEffectFactory._effects[effect_type] = DelayEffect()
else:
raise ValueError("Effect type not supported")
return GuitarEffectFactory._effects[effect_type]
# The Extrinsic state
class GuitarSettings:
def __init__(self, intensity, speed):
self.intensity = intensity
self.speed = speed
def __str__(self):
return f"Intensity: {self.intensity}, Speed: {self.speed}"
# Client
if __name__ == "__main__":
chorus = GuitarEffectFactory.get_effect("Chorus")
delay = GuitarEffectFactory.get_effect("Delay")
settings1 = GuitarSettings(5, 10)
settings2 = GuitarSettings(7, 12)
chorus.apply_effect(settings1)
delay.apply_effect(settings2)
chorus.apply_effect(settings2)
Summary
The Flyweight pattern optimizes memory usage by sharing common parts of state between multiple objects, rather than keeping independent copies of the same information. This is achieved by separating the intrinsic state (which is shared and immutable) from the extrinsic state (which is unique to each object and passed to the flyweight methods). This pattern is particularly useful when dealing with a large number of similar objects, such as in graphical applications, text editors, or simulations.
The Proxy design pattern helps you provide a surrogate or placeholder for another object to control access to it. Instead of letting clients access the real object directly, you create a proxy that intercepts requests. This keeps the client unaware of whether it's talking to the real object or the proxy. As a result, you can add lazy initialization, logging, or caching without changing the real object or the client.
Key Concepts
- Subject: The common interface for RealSubject and Proxy, allowing a Proxy to be used anywhere a RealSubject is expected.
- RealSubject: The real object that the proxy represents.
- Proxy: The class that maintains a reference to the real subject and may perform additional operations before or after delegating to it.
- Client: The class that interacts with the Subject through the Proxy.
Benefits
- It supports lazy initialization, creating expensive objects only when needed.
- It allows adding behavior (logging, caching) without modifying the real object.
Let's make the Proxy pattern more concrete with a practical example. Imagine we have an amplifier cabinet simulator that loads large impulse response (IR) files. Loading these files is expensive, so we use a Virtual Proxy to delay loading until the cabinet is actually used for the first time.
The diagram below illustrates how the Proxy pattern is applied in our cabinet simulator scenario: the CabinetProxy implements the same interface as HighQualityCabinet and delays the expensive loading operation until simulate() is called:
classDiagram
class Cabinet {
<<interface>>
+simulate(signal: String) String
}
class HighQualityCabinet {
-irFile: String
-loaded: boolean
+simulate(signal: String) String
-loadImpulseResponse()
}
class CabinetProxy {
-cabinet: Cabinet
-irFile: String
+simulate(signal: String) String
}
Cabinet <|.. HighQualityCabinet
Cabinet <|.. CabinetProxy
CabinetProxy o-- Cabinet
Now, let's see how this pattern comes to life in code by implementing a cabinet proxy that lazily loads the expensive impulse response file.
// The Subject interface
interface Cabinet {
String simulate(String signal);
}
// The RealSubject - expensive to create
class HighQualityCabinet implements Cabinet {
private String irFile;
private boolean loaded = false;
public HighQualityCabinet(String irFile) {
this.irFile = irFile;
loadImpulseResponse();
}
private void loadImpulseResponse() {
System.out.println("Loading impulse response from " + irFile + "...");
// Simulate expensive file loading
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
loaded = true;
System.out.println("Impulse response loaded successfully.");
}
@Override
public String simulate(String signal) {
return "Cabinet simulation of '" + signal + "' using " + irFile;
}
}
// The Proxy - delays creation of expensive object
class CabinetProxy implements Cabinet {
private Cabinet cabinet;
private String irFile;
public CabinetProxy(String irFile) {
this.irFile = irFile;
// Note: We don't create the real cabinet here
System.out.println("Proxy created for " + irFile + " (not loaded yet)");
}
@Override
public String simulate(String signal) {
if (cabinet == null) {
System.out.println("First use - initializing cabinet...");
cabinet = new HighQualityCabinet(irFile);
}
return cabinet.simulate(signal);
}
}
// Client
public class Main {
public static void main(String[] args) {
// Create proxies - no expensive loading happens yet
Cabinet mesa = new CabinetProxy("mesa_oversized.ir");
Cabinet marshall = new CabinetProxy("marshall_1960.ir");
System.out.println("\n--- Using first cabinet ---");
System.out.println(mesa.simulate("clean guitar"));
System.out.println("\n--- Using first cabinet again (already loaded) ---");
System.out.println(mesa.simulate("distorted guitar"));
System.out.println("\n--- Marshall cabinet still not loaded ---");
// marshall.simulate() would trigger loading only when called
}
}
// The Subject interface
interface Cabinet {
fun simulate(signal: String): String
}
// The RealSubject - expensive to create
class HighQualityCabinet(private val irFile: String) : Cabinet {
init {
loadImpulseResponse()
}
private fun loadImpulseResponse() {
println("Loading impulse response from $irFile...")
Thread.sleep(2000) // Simulate expensive loading
println("Impulse response loaded successfully.")
}
override fun simulate(signal: String): String {
return "Cabinet simulation of '$signal' using $irFile"
}
}
// The Proxy - delays creation of expensive object
class CabinetProxy(private val irFile: String) : Cabinet {
private var cabinet: Cabinet? = null
init {
println("Proxy created for $irFile (not loaded yet)")
}
override fun simulate(signal: String): String {
if (cabinet == null) {
println("First use - initializing cabinet...")
cabinet = HighQualityCabinet(irFile)
}
return cabinet!!.simulate(signal)
}
}
// Client
fun main() {
// Create proxies - no expensive loading happens yet
val mesa = CabinetProxy("mesa_oversized.ir")
val marshall = CabinetProxy("marshall_1960.ir")
println("\n--- Using first cabinet ---")
println(mesa.simulate("clean guitar"))
println("\n--- Using first cabinet again (already loaded) ---")
println(mesa.simulate("distorted guitar"))
println("\n--- Marshall cabinet still not loaded ---")
}
// The Subject interface
interface Cabinet {
simulate(signal: string): string;
}
// The RealSubject - expensive to create
class HighQualityCabinet implements Cabinet {
private irFile: string;
constructor(irFile: string) {
this.irFile = irFile;
this.loadImpulseResponse();
}
private loadImpulseResponse(): void {
console.log(`Loading impulse response from ${this.irFile}...`);
// Simulate expensive loading (in real code, this would be async)
const start = Date.now();
while (Date.now() - start < 100) {} // Brief delay for demo
console.log("Impulse response loaded successfully.");
}
simulate(signal: string): string {
return `Cabinet simulation of '${signal}' using ${this.irFile}`;
}
}
// The Proxy - delays creation of expensive object
class CabinetProxy implements Cabinet {
private cabinet: Cabinet | null = null;
private irFile: string;
constructor(irFile: string) {
this.irFile = irFile;
console.log(`Proxy created for ${irFile} (not loaded yet)`);
}
simulate(signal: string): string {
if (this.cabinet === null) {
console.log("First use - initializing cabinet...");
this.cabinet = new HighQualityCabinet(this.irFile);
}
return this.cabinet.simulate(signal);
}
}
// Client
const mesa = new CabinetProxy("mesa_oversized.ir");
const marshall = new CabinetProxy("marshall_1960.ir");
console.log("\n--- Using first cabinet ---");
console.log(mesa.simulate("clean guitar"));
console.log("\n--- Using first cabinet again (already loaded) ---");
console.log(mesa.simulate("distorted guitar"));
console.log("\n--- Marshall cabinet still not loaded ---");
// The Subject interface
abstract class Cabinet {
String simulate(String signal);
}
// The RealSubject - expensive to create
class HighQualityCabinet implements Cabinet {
final String irFile;
HighQualityCabinet(this.irFile) {
_loadImpulseResponse();
}
void _loadImpulseResponse() {
print('Loading impulse response from $irFile...');
// Simulate expensive loading
final stopwatch = Stopwatch()..start();
while (stopwatch.elapsedMilliseconds < 100) {}
print('Impulse response loaded successfully.');
}
@override
String simulate(String signal) {
return "Cabinet simulation of '$signal' using $irFile";
}
}
// The Proxy - delays creation of expensive object
class CabinetProxy implements Cabinet {
Cabinet? _cabinet;
final String irFile;
CabinetProxy(this.irFile) {
print('Proxy created for $irFile (not loaded yet)');
}
@override
String simulate(String signal) {
if (_cabinet == null) {
print('First use - initializing cabinet...');
_cabinet = HighQualityCabinet(irFile);
}
return _cabinet!.simulate(signal);
}
}
// Client
void main() {
// Create proxies - no expensive loading happens yet
final mesa = CabinetProxy('mesa_oversized.ir');
final marshall = CabinetProxy('marshall_1960.ir');
print('\n--- Using first cabinet ---');
print(mesa.simulate('clean guitar'));
print('\n--- Using first cabinet again (already loaded) ---');
print(mesa.simulate('distorted guitar'));
print('\n--- Marshall cabinet still not loaded ---');
}
// The Subject interface
protocol Cabinet {
func simulate(signal: String) -> String
}
// The RealSubject - expensive to create
class HighQualityCabinet: Cabinet {
private let irFile: String
init(irFile: String) {
self.irFile = irFile
loadImpulseResponse()
}
private func loadImpulseResponse() {
print("Loading impulse response from \(irFile)...")
Thread.sleep(forTimeInterval: 0.1) // Simulate expensive loading
print("Impulse response loaded successfully.")
}
func simulate(signal: String) -> String {
return "Cabinet simulation of '\(signal)' using \(irFile)"
}
}
// The Proxy - delays creation of expensive object
class CabinetProxy: Cabinet {
private var cabinet: Cabinet?
private let irFile: String
init(irFile: String) {
self.irFile = irFile
print("Proxy created for \(irFile) (not loaded yet)")
}
func simulate(signal: String) -> String {
if cabinet == nil {
print("First use - initializing cabinet...")
cabinet = HighQualityCabinet(irFile: irFile)
}
return cabinet!.simulate(signal: signal)
}
}
// Client
let mesa = CabinetProxy(irFile: "mesa_oversized.ir")
let marshall = CabinetProxy(irFile: "marshall_1960.ir")
print("\n--- Using first cabinet ---")
print(mesa.simulate(signal: "clean guitar"))
print("\n--- Using first cabinet again (already loaded) ---")
print(mesa.simulate(signal: "distorted guitar"))
print("\n--- Marshall cabinet still not loaded ---")
import time
# The Subject interface
class Cabinet:
def simulate(self, signal: str) -> str:
pass
# The RealSubject - expensive to create
class HighQualityCabinet(Cabinet):
def __init__(self, ir_file: str):
self.ir_file = ir_file
self._load_impulse_response()
def _load_impulse_response(self):
print(f"Loading impulse response from {self.ir_file}...")
time.sleep(0.1) # Simulate expensive loading
print("Impulse response loaded successfully.")
def simulate(self, signal: str) -> str:
return f"Cabinet simulation of '{signal}' using {self.ir_file}"
# The Proxy - delays creation of expensive object
class CabinetProxy(Cabinet):
def __init__(self, ir_file: str):
self.ir_file = ir_file
self._cabinet = None
print(f"Proxy created for {ir_file} (not loaded yet)")
def simulate(self, signal: str) -> str:
if self._cabinet is None:
print("First use - initializing cabinet...")
self._cabinet = HighQualityCabinet(self.ir_file)
return self._cabinet.simulate(signal)
# Client
if __name__ == "__main__":
# Create proxies - no expensive loading happens yet
mesa = CabinetProxy("mesa_oversized.ir")
marshall = CabinetProxy("marshall_1960.ir")
print("\n--- Using first cabinet ---")
print(mesa.simulate("clean guitar"))
print("\n--- Using first cabinet again (already loaded) ---")
print(mesa.simulate("distorted guitar"))
print("\n--- Marshall cabinet still not loaded ---")
Summary
The Proxy pattern provides a surrogate that controls access to another object. The Virtual Proxy shown here delays expensive object creation until it's actually needed, which is useful when loading large resources like audio files, images, or complex simulations.