Skip to content

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 pattern is a structural design pattern that allows objects with incompatible interfaces to collaborate. It acts as a bridge between two incompatible interfaces by converting the interface of one class into another interface that clients expect.

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#

  • Allows classes with incompatible interfaces to work together.
  • Promotes code reusability.
  • Follows the Single Responsibility Principle.

The following diagram illustrates the Adapter pattern with our guitar tuner example, showing how TunerAdapter wraps the old AnalogTuner to make it compatible with the modern DigitalTuner interface:

classDiagram
    class DigitalTuner {
        <<interface>>
        +tune()
    }
    class TunerAdapter {
        -analogTuner: AnalogTuner
        +tune()
    }
    class AnalogTuner {
        +tuneAnalog()
    }

    DigitalTuner <|.. TunerAdapter
    TunerAdapter o-- AnalogTuner

The TunerAdapter acts as a translator, converting the old analog tuner interface into one that modern digital systems can understand. Let's see this pattern in action across different languages.

Example#

Let's consider a scenario where we have an old guitar tuner system that needs to be adapted to work with a new digital tuner 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 pattern is a structural design pattern that lets you split a large class or a set of closely related classes into two separate hierarchies—abstraction and implementation—which can be developed independently of each other.

Key Concepts#

  • Abstraction: Defines the interface for the "control" part of the two class hierarchies.
  • Implementation: Defines the interface for the "implementation" part of the two class hierarchies.
  • Refined Abstraction: Extends the abstraction interface.
  • Concrete Implementation: Implements the implementation interface.

Benefits#

  • Separates abstraction from implementation.
  • Allows both abstraction and implementation to be extended independently.
  • Hides implementation details from clients.

The following diagram demonstrates the Bridge pattern with our amplifier and effects example, showing how amplifier types (abstraction) and effect types (implementation) can vary independently:

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

The Bridge decouples amplifier types from effect types, allowing you to pair any amplifier with any effect independently. Let's explore this flexibility across different languages.

Example#

Let's consider a scenario where we have different types of guitar amplifiers and their effects.

// 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 pattern is a structural design pattern that lets you compose objects into tree structures to represent part-whole hierarchies. Composite lets clients treat individual objects and compositions of objects uniformly.

Key Concepts#

  • Component: The base interface or abstract class for all objects in the composition. It declares the common operations for both simple (leaf) and complex (composite) objects.
  • Leaf: Represents the end objects of a composition. A leaf has no children.
  • Composite: A container that holds child components. It implements the Component interface and defines methods to manage child components.
  • Client: Manipulates the objects in the composition through the Component interface.

Benefits#

  • Defines class hierarchies that contain primitive objects and composite objects.
  • Simplifies the client code, as clients can treat individual elements and compositions uniformly.
  • Makes it easier to add new kinds of components.

The following diagram illustrates the Composite pattern with our guitar effects example, showing how individual effects (leaves) and effects chains (composites) share the same interface, enabling tree-like 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

This pattern enables you to build complex effect chains by treating individual effects and chains uniformly. Let's see how this works across different languages.

Example#

Let's consider a guitar effects chain. An effects chain can consist of individual effects (e.g., reverb, delay) or other effects 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 pattern is a structural design pattern that allows behavior to be added to individual objects, either statically or dynamically, without affecting the behavior of other objects from the same class. It achieves this by wrapping the original object with one or more decorator objects, which add functionality.

Key Concepts#

  • Component: The interface or abstract class defining the object to which additional responsibilities can be attached.
  • Concrete Component: The base object to which decorators can add behavior.
  • Decorator: An abstract class that implements the Component interface and holds a reference to a Component object.
  • Concrete Decorators: Classes that extend the Decorator and add specific behaviors to the component.
  • Client: The class that uses the decorated component.

Benefits#

  • Open/Closed Principle: You can add new behaviors without modifying existing classes.
  • Single Responsibility Principle: Decorators add responsibilities, keeping classes focused.
  • Flexibility: Behaviors can be added or removed at runtime.

The following diagram illustrates the Decorator pattern with our amplifier example, showing how decorators wrap the base amplifier to dynamically add effects like reverb and chorus:

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

The Decorator wraps the component and adds new behavior while maintaining the same interface, enabling you to stack multiple effects dynamically. Let's see this layering in action.

Example#

Let's consider a guitar amplifier. We can add decorators to modify the sound of the amplifier, such as adding reverb or chorus effects.

// 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 pattern is a structural design pattern that provides a simplified interface to a complex system of classes, library, or framework. It hides the complexities of the system and provides a single entry point for clients to interact with it.

Key Concepts#

  • Facade: Provides a simple interface to the complex subsystem. It knows which subsystem classes are responsible for a request.
  • Subsystem: Consists of one or more classes that implement subsystem functionality. It handles the actual work.
  • Client: Interacts with the subsystem through the Facade.

Benefits#

  • Simplifies the interface of a complex system.
  • Decouples the client from the subsystem components.
  • Promotes loose coupling between the client and the subsystem.

The following diagram illustrates the Facade pattern with our guitar recording example, showing how GuitarRecordingFacade provides a simple interface that orchestrates complex subsystems:

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

The Facade simplifies the complex recording workflow by hiding the intricate coordination of multiple subsystems behind a single, easy-to-use interface. Let's see this simplification in action.

Example#

Let's consider a guitar recording system. The system involves multiple complex components like microphone preamps, audio interfaces, and digital audio workstations (DAWs). The Facade pattern can provide a simplified interface for recording a guitar track.

// 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 pattern is a structural design pattern that aims to minimize memory usage or computational expenses by sharing as much as possible with related objects. It is particularly useful when dealing with a large number of similar objects.

Key Concepts#

  • Flyweight: Declares an interface through which flyweights can receive and act on extrinsic state.
  • Concrete Flyweight: Implements the Flyweight interface and stores intrinsic state. It must be sharable.
  • Unshared Concrete Flyweight: Not all Flyweights need to be shared. The Flyweight interface enables sharing, but it doesn't enforce it.
  • Flyweight Factory: Creates and manages flyweight objects. It ensures that flyweights are shared properly.
  • Client: Maintains a reference to flyweights and computes or stores extrinsic state.

Benefits#

  • Reduces memory usage by sharing objects.
  • Improves performance by reducing the number of objects created.
  • Separates intrinsic and extrinsic state, promoting code reusability.

The following diagram illustrates the Flyweight pattern with our guitar effects example, showing how GuitarEffectFactory manages shared effect instances while external settings vary:

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

The Flyweight Factory ensures effect objects are reused, storing intrinsic state (effect type) in shared flyweights while extrinsic state (settings) is passed in at runtime. Let's see this memory optimization in action.

Example#

Let's consider a guitar effects system where we have multiple guitar pedals. Each pedal has some intrinsic state (e.g., the type of effect) and extrinsic state (e.g., the settings applied by the user). The Flyweight pattern can be used to share the intrinsic state among multiple pedal instances.

// 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 pattern is a structural design pattern that provides a surrogate or placeholder for another object to control access to it. It acts as an intermediary, allowing you to perform additional operations before or after the request gets to the original object.

Key Concepts#

  • Subject: Defines the common interface for RealSubject and Proxy so that a Proxy can be used anywhere a RealSubject is expected.
  • RealSubject: Defines the real object that the proxy represents.
  • Proxy: Maintains a reference that lets the proxy access the real subject. The Proxy may perform additional operations (e.g., lazy initialization, access control) before or after passing the request to the real object.
  • Client: Interacts with the Subject through the Proxy.

Benefits#

  • Controls access to the RealSubject.
  • Can perform additional operations before or after the request gets to the RealSubject.
  • Supports lazy initialization.

The following diagram illustrates the Proxy pattern with our guitar recording example, showing how RecordingProxy controls access to HighQualityRecording with authorization and lazy loading:

classDiagram
    class GuitarRecording {
        <<interface>>
        +play()
    }
    class HighQualityRecording {
        -filePath: String
        +play()
        -loadRecording()
    }
    class RecordingProxy {
        -recording: GuitarRecording
        -filePath: String
        -isAuthorized: boolean
        +play()
    }

    GuitarRecording <|.. HighQualityRecording
    GuitarRecording <|.. RecordingProxy
    RecordingProxy o-- GuitarRecording

The Proxy intercepts requests, checking authorization and lazily creating the expensive HighQualityRecording only when needed and permitted. Let's see this access control in action.

Example#

Let's consider a scenario where we want to control access to a high-quality guitar recording. The Proxy pattern can be used to ensure that only authorized users can access the recording.

// The Subject interface
interface GuitarRecording {
    void play();
}

// The RealSubject
class HighQualityRecording implements GuitarRecording {
    private String filePath;

    public HighQualityRecording(String filePath) {
        this.filePath = filePath;
        loadRecording();
    }

    private void loadRecording() {
        System.out.println("Loading high-quality recording from " + filePath);
        // Simulate loading a large file
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("Recording loaded.");
    }

    @Override
    public void play() {
        System.out.println("Playing high-quality recording from " + filePath);
    }
}

// The Proxy
class RecordingProxy implements GuitarRecording {
    private GuitarRecording recording;
    private String filePath;
    private boolean isAuthorized;

    public RecordingProxy(String filePath, boolean isAuthorized) {
        this.filePath = filePath;
        this.isAuthorized = isAuthorized;
    }

    @Override
    public void play() {
        if (isAuthorized) {
            if (recording == null) {
                recording = new HighQualityRecording(filePath); // Lazy initialization
            }
            recording.play();
        } else {
            System.out.println("Access denied. User is not authorized to play the recording.");
        }
    }
}

// Client
public class Main {
    public static void main(String[] args) {
        RecordingProxy authorizedRecording = new RecordingProxy("hq_recording.wav", true);
        authorizedRecording.play();

        RecordingProxy unauthorizedRecording = new RecordingProxy("hq_recording.wav", false);
        unauthorizedRecording.play();
    }
}
// The Subject interface
interface GuitarRecording {
    fun play()
}

// The RealSubject
class HighQualityRecording(private val filePath: String) : GuitarRecording {
    init {
        loadRecording()
    }

    private fun loadRecording() {
        println("Loading high-quality recording from $filePath")
        // Simulate loading a large file
        Thread.sleep(2000)
        println("Recording loaded.")
    }

    override fun play() {
        println("Playing high-quality recording from $filePath")
    }
}

// The Proxy
class RecordingProxy(private val filePath: String, private val isAuthorized: Boolean) : GuitarRecording {
    private var recording: GuitarRecording? = null

    override fun play() {
        if (isAuthorized) {
            if (recording == null) {
                recording = HighQualityRecording(filePath) // Lazy initialization
            }
            recording?.play()
        } else {
            println("Access denied. User is not authorized to play the recording.")
        }
    }
}

// Client
fun main() {
    val authorizedRecording = RecordingProxy("hq_recording.wav", true)
    authorizedRecording.play()

    val unauthorizedRecording = RecordingProxy("hq_recording.wav", false)
    unauthorizedRecording.play()
}
// The Subject interface
interface GuitarRecording {
    play(): void;
}

// The RealSubject
class HighQualityRecording implements GuitarRecording {
    private filePath: string;

    constructor(filePath: string) {
        this.filePath = filePath;
        this.loadRecording();
    }

    private loadRecording(): void {
        console.log("Loading high-quality recording from " + this.filePath);
        // Simulate loading a large file
        setTimeout(() => {
            console.log("Recording loaded.");
        }, 2000);
    }

    play(): void {
        console.log("Playing high-quality recording from " + this.filePath);
    }
}

// The Proxy
class RecordingProxy implements GuitarRecording {
    private recording: GuitarRecording | null = null;
    private filePath: string;
    private isAuthorized: boolean;

    constructor(filePath: string, isAuthorized: boolean) {
        this.filePath = filePath;
        this.isAuthorized = isAuthorized;
    }

    play(): void {
        if (this.isAuthorized) {
            if (this.recording === null) {
                this.recording = new HighQualityRecording(this.filePath); // Lazy initialization
            }
            this.recording.play();
        } else {
            console.log("Access denied. User is not authorized to play the recording.");
        }
    }
}

// Client
const authorizedRecording = new RecordingProxy("hq_recording.wav", true);
authorizedRecording.play();

const unauthorizedRecording = new RecordingProxy("hq_recording.wav", false);
unauthorizedRecording.play();
// The Subject interface
abstract class GuitarRecording {
  void play();
}

// The RealSubject
class HighQualityRecording implements GuitarRecording {
  final String filePath;

  HighQualityRecording(this.filePath) {
    loadRecording();
  }

  void loadRecording() {
    print('Loading high-quality recording from $filePath');
    // Simulate loading a large file
    Future.delayed(Duration(seconds: 2), () {
      print('Recording loaded.');
    });
  }

  @override
  void play() {
    print('Playing high-quality recording from $filePath');
  }
}

// The Proxy
class RecordingProxy implements GuitarRecording {
  GuitarRecording? recording;
  final String filePath;
  final bool isAuthorized;

  RecordingProxy(this.filePath, this.isAuthorized);

  @override
  void play() {
    if (isAuthorized) {
      recording ??= HighQualityRecording(filePath); // Lazy initialization
      recording?.play();
    } else {
      print('Access denied. User is not authorized to play the recording.');
    }
  }
}

// Client
void main() {
  final authorizedRecording = RecordingProxy('hq_recording.wav', true);
  authorizedRecording.play();

  final unauthorizedRecording = RecordingProxy('hq_recording.wav', false);
  unauthorizedRecording.play();
}
// The Subject interface
protocol GuitarRecording {
    func play()
}

// The RealSubject
class HighQualityRecording: GuitarRecording {
    private let filePath: String

    init(filePath: String) {
        self.filePath = filePath
        loadRecording()
    }

    private func loadRecording() {
        print("Loading high-quality recording from \(filePath)")
        // Simulate loading a large file
        Thread.sleep(forTimeInterval: 2)
        print("Recording loaded.")
    }

    func play() {
        print("Playing high-quality recording from \(filePath)")
    }
}

// The Proxy
class RecordingProxy: GuitarRecording {
    private var recording: GuitarRecording?
    private let filePath: String
    private let isAuthorized: Bool

    init(filePath: String, isAuthorized: Bool) {
        self.filePath = filePath
        self.isAuthorized = isAuthorized
    }

    func play() {
        if isAuthorized {
            if recording == nil {
                recording = HighQualityRecording(filePath: filePath) // Lazy initialization
            }
            recording?.play()
        } else {
            print("Access denied. User is not authorized to play the recording.")
        }
    }
}

// Client
let authorizedRecording = RecordingProxy(filePath: "hq_recording.wav", isAuthorized: true)
authorizedRecording.play()

let unauthorizedRecording = RecordingProxy(filePath: "hq_recording.wav", isAuthorized: false)
unauthorizedRecording.play()
import time

# The Subject interface
class GuitarRecording:
    def play(self):
        pass

# The RealSubject
class HighQualityRecording(GuitarRecording):
    def __init__(self, file_path):
        self.file_path = file_path
        self.load_recording()

    def load_recording(self):
        print(f"Loading high-quality recording from {self.file_path}")
        # Simulate loading a large file
        time.sleep(2)
        print("Recording loaded.")

    def play(self):
        print(f"Playing high-quality recording from {self.file_path}")

# The Proxy
class RecordingProxy(GuitarRecording):
    def __init__(self, file_path, is_authorized):
        self.file_path = file_path
        self.is_authorized = is_authorized
        self._recording = None

    def play(self):
        if self.is_authorized:
            if self._recording is None:
                self._recording = HighQualityRecording(self.file_path)  # Lazy initialization
            self._recording.play()
        else:
            print("Access denied. User is not authorized to play the recording.")

# Client
authorized_recording = RecordingProxy("hq_recording.wav", True)
authorized_recording.play()

unauthorized_recording = RecordingProxy("hq_recording.wav", False)
unauthorized_recording.play()

Summary#

The Proxy pattern provides a way to control access to an object, defer its initialization, or add additional functionality when the object is accessed. It is useful when you need to protect a real object from direct access, when the real object is expensive to create, or when you want to add additional behavior when the object is accessed.