tech-notes-and-questions

Structural Design Patterns

[definition, purpose, use cases, design, example, implementation]

  1. Adapter - converts one interface to another, allowing incompatible interfaces to work together
  2. Bridge - decouples an abstraction from its implementation, enabling them to vary independently
  3. Composite - composes objects into tree structures to represent part-whole hierarchies
  4. Decorator - adds responsibilities to an object dynamically without altering its structure
  5. Facade - provides a simplified interface to a complex system of classes
  6. Flyweight - reduces memory usage by sharing common parts of objects, making large numbers of fine-grained objects more efficient
  7. Proxy - controls access to another object, providing additional functionality like lazy initialization or security

1. Adapter Pattern

Definition

Purpose

This is helpful when you have a legacy system or an external library that doesn’t match your application’s interfaces.

Use cases

1. Testing and Mocking

2. Communication Protocols

3. Third-Party Library Integration

Design

Example

Imagine you have an application that works with AudioPlayer, but now you need to integrate a VideoPlayer. Instead of modifying the existing AudioPlayer class, you can create an adapter for VideoPlayer to work with AudioPlayer without altering the original structure.

Implementation

  1. Define the Target interface (the new interface your client expects)
interface AdvancedMediaPlayer {
    void play(String audioType, String fileName);
}
  1. Create the Adaptee (The class that needs to be adapted)
class LegacyMediaPlayer {
    void playMp4(String fileName) {
        System.out.println("Playing mp4 file: " + fileName);
    }

    void playVlc(String fileName) {
        System.out.println("Playing vlc file: " + fileName);
    }
}
  1. Create the Adapter Class - The adapter class implements the AdvancedMediaPlayer interface and uses the LegacyMediaPlayer to provide the functionality.
class MediaAdapter implements AdvancedMediaPlayer {
    LegacyMediaPlayer legacyMediaPlayer;

    public MediaAdapter(String audioType) {
        if (audioType.equalsIgnoreCase("vlc")) {
            legacyMediaPlayer = new LegacyMediaPlayer();
        } else if (audioType.equalsIgnoreCase("mp4")) {
            legacyMediaPlayer = new LegacyMediaPlayer();
        }
    }

    @Override
    void play(String audioType, String fileName) {
        if (audioType.equalsIgnoreCase("vlc")) {
            legacyMediaPlayer.playVlc(fileName);
        } else if (audioType.equalsIgnoreCase("mp4")) {
            legacyMediaPlayer.playMp4(fileName);
        }
    }
}    
  1. Client code that uses the Adapter
public class AudioPlayer implements  AdvancedMediaPlayer {
    MediaAdapter mediaAdapter;
    
    @Override
    void play (String fileType, String fileName) {
        // Playing mp3 directly
        if (audioType.equalsIgnoreCase("mp3")) {
            System.out.println("Playing mp3 file: " + fileName);
        }
        // Use adapter to play other file formats
        else if (audioType.equalsIgnoreCase("vlc") || audioType.equalsIgnoreCase("mp4")) {
            mediaAdapter = new MediaAdapter(audioType);
            mediaAdapter.play(audioType, fileName);
        } else {
            System.out.println("Invalid media type: " + audioType + " format not supported");
        }
    }
}
  1. Testing the Adapter Pattern
public static void main (String[] args) {
    AudioPlayer audioPlayer = new AudioPlayer();
    
    audioPlayer.play("mp3", "song.mp3");   // Playing mp3 file: song.mp3
    audioPlayer.play("mp4", "video.mp4");  // Playing mp4 file: video.mp4
    audioPlayer.play("vlc", "movie.vlc");  // Playing vlc file: movie.vlc
    audioPlayer.play("avi", "film.avi");   // Invalid media type: avi format not supported
}


</br>

2. Bridge Pattern

Definition

A design pattern that separates an abstraction from its implementation, allowing the two to vary independently.

Purpose

This is useful when you want to avoid a permanent binding between an abstraction and its implementation.

Use cases

1. UI Frameworks

2. Database Access Layers

3. Payment Processing Systems

Design

Example

Let’s consider a simple example involving shapes and colors. Shapes don’t know how to color themselves.

Implementation

  1. Define the Implementor Interface
interface Color {
    void applyColor();
}
  1. Create Concrete Implementors
class RedColor implements Color {
    @Override
    public void applyColor() {
        System.out.println("Applying red color.");
    }
}
class GreenColor implements Color {
    @Override
    public void applyColor() {
        System.out.println("Applying green color.");
    }
}
  1. Define the Abstraction
abstract class Shape {
    protected Color color;

    protected Shape(Color color) {
        this.color = color;
    }

    abstract void draw();
}
  1. Create Refined Abstractions
class Circle extends Shape {
    public Circle(Color color) {
        super(color);
    }

    @Override
    void draw() {
        System.out.print("Drawing Circle. ");
        color.applyColor();
    }
}
class Square extends Shape {
    public Square(Color color) {
        super(color);
    }

    @Override
    void draw() {
        System.out.print("Drawing Square. ");
        color.applyColor();
    }
}
  1. Client code to demonstrate the usage
public static void main(String[] args) {
        Shape redCircle = new Circle(new RedColor());
        Shape greenSquare = new Square(new GreenColor());

        redCircle.draw(); // Output: Drawing Circle. Applying red color.
        greenSquare.draw(); // Output: Drawing Square. Applying green color.
}


</br>

3. Composite Pattern

Definition

Benefits

Purpose

When you want to create a tree structured to composition of classes, while treating the individual objects and the composition uniformly.

Use cases

Design

Example

Let’s create a file system example where directories can contain files and other directories.

Implementation

  1. Define the Component Interface
interface FileSystemComponent {
    void showDetails();
}
  1. Create Leaf Class
class File implements FileSystemComponent {
    private String name;

    public File(String name) {
        this.name = name;
    }

    @Override
    public void showDetails() {
        System.out.println("File: " + name);
    }
}
  1. Create Composite Class
class Directory implements FileSystemComponent {
    private String name;
    private List<FileSystemComponent> components = new ArrayList<>();

    public Directory(String name) {
        this.name = name;
    }

    public void addComponent(FileSystemComponent component) {
        components.add(component);
    }

    public void removeComponent(FileSystemComponent component) {
        components.remove(component);
    }

    @Override
    public void showDetails() {
        System.out.println("Directory: " + name);
        for (FileSystemComponent component : components) {
            component.showDetails();
        }
    }
}
  1. Client code to demonstrate the usage
public static void main(String[] args) {
        FileSystemComponent file1 = new File("File1.txt");
        FileSystemComponent file2 = new File("File2.txt");
        
        Directory directory1 = new Directory("Directory1");
        directory1.addComponent(file1);
        directory1.addComponent(file2);
        
        FileSystemComponent file3 = new File("File3.txt");
        
        Directory directory2 = new Directory("Directory2");
        directory2.addComponent(directory1);
        directory2.addComponent(file3);

        // Display the file system structure
        directory2.showDetails();
        
        // Output-------
        /*
                Directory: Directory2
                    Directory: Directory1
                    File: File1.txt
                    File: File2.txt
                    File: File3.txt
         */
    }


</br>

4. Decorator Pattern

Definition

Purpose

Use cases

Design

Example

Implementation


</br> Purpose: Dynamically adds responsibilities to objects by wrapping them in additional functionality without altering the object itself. It provides an alternative to subclassing for extending behavior.

Application: Adding additional features (e.g., scrollbars or borders) to GUI components.

Example: Say you want to take an order for a pizza, with many possible combinations of toppings. Creating subclass to superclass Pizza for each combination would create Class Explosion. So we create a Decorative Layer on top that could accommodate multiple combinations but return the same class ultimately. This decorating layer will have both ‘has-a’ & ‘is-a’ relationships with the base class.

Decorator Design Pattern


</br>

5. Facade Pattern

Definition

Benefits

Purpose

This pattern is particularly useful when you want to reduce the complexity of interacting with multiple classes or libraries.

Use cases

Design

Example

Let’s create an example for a home theater system that consists of several components: a DVD player, a projector, and a sound system.

Implementation

  1. Create Subsystem Classes
class DVDPlayer {
    public void on() {
        System.out.println("DVD Player is ON");
    }
    public void play(String movie) {
        System.out.println("Playing movie: " + movie);
    }
    public void stop() {
        System.out.println("DVD Player stopped");
    }
    public void off() {
        System.out.println("DVD Player is OFF");
    }
}
class Projector {
    public void on() {
        System.out.println("Projector is ON");
    }
    public void setInput(String input) {
        System.out.println("Projector input set to: " + input);
    }
    public void off() {
        System.out.println("Projector is OFF");
    }
}
class SoundSystem {
    public void on() {
        System.out.println("Sound System is ON");
    }
    public void setVolume(int level) {
        System.out.println("Sound System volume set to: " + level);
    }
    public void off() {
        System.out.println("Sound System is OFF");
    }
}
  1. Create the Facade Class
class HomeTheaterFacade {
    private DVDPlayer dvdPlayer;
    private Projector projector;
    private SoundSystem soundSystem;

    public HomeTheaterFacade(DVDPlayer dvdPlayer, Projector projector, SoundSystem soundSystem) {
        this.dvdPlayer = dvdPlayer;
        this.projector = projector;
        this.soundSystem = soundSystem;
    }

    public void watchMovie(String movie) {
        System.out.println("Get ready to watch a movie...");
        projector.on();
        projector.setInput("DVD");
        soundSystem.on();
        soundSystem.setVolume(10);
        dvdPlayer.on();
        dvdPlayer.play(movie);
    }

    public void endMovie() {
        System.out.println("Shutting movie theater down...");
        dvdPlayer.stop();
        dvdPlayer.off();
        soundSystem.off();
        projector.off();
    }
}

  1. Client code to demonstrate the usage
public static void main(String[] args) {
        DVDPlayer dvdPlayer = new DVDPlayer();
        Projector projector = new Projector();
        SoundSystem soundSystem = new SoundSystem();

        HomeTheaterFacade homeTheater = new HomeTheaterFacade(dvdPlayer, projector, soundSystem);

        homeTheater.watchMovie("Inception");
        homeTheater.endMovie();
}


</br>

6. Flyweight Pattern

Definition

A pattern that aims to minimize memory usage by sharing common data among multiple objects.

Purpose

Use cases

Design

Example

Let’s create a simple example of a text editor that manages different font styles.

Implementation

  1. Define the Flyweight Interface
interface Font {
    void display(int size, String color);
}
  1. Create Concrete Flyweight Classes
class ConcreteFont implements Font {
    private String fontType; // Intrinsic state

    public ConcreteFont(String fontType) {
        this.fontType = fontType;
    }

    @Override
    public void display(int size, String color) {
        System.out.println("Font: " + fontType + ", Size: " + size + ", Color: " + color);
    }
}
  1. Create a Flyweight Factory
class FontFactory {
    private static final HashMap<String, Font> fontMap = new HashMap<>();

    public static Font getFont(String fontType) {
        Font font = fontMap.get(fontType);
        if (font == null) {
            font = new ConcreteFont(fontType);
            fontMap.put(fontType, font);
            System.out.println("Creating new font: " + fontType);
        }
        return font;
    }
}
  1. Client code to demonstrate the usage
public static void main(String[] args) {
        Font font1 = FontFactory.getFont("Arial");
        font1.display(12, "Red");

        Font font2 = FontFactory.getFont("Arial");
        font2.display(14, "Blue");

        Font font3 = FontFactory.getFont("Times New Roman");
        font3.display(16, "Green");

        // Both font1 and font2 point to the same Arial font instance
        System.out.println("font1 == font2: " + (font1 == font2));
}


</br>

7. Proxy Pattern

Definition

Purpose

Use cases

Design

Example

Let’s illustrate the Proxy Design Pattern with a simple example involving image loading.

Implementation

  1. Create the Subject Interface
public interface Image {
    void display();
}
  1. Implement the RealObject
public class RealImage implements Image {
    private String filename;

    public RealImage(String filename) {
        loadImageFromDisk();
        this.filename = filename;
    }

    private void loadImageFromDisk() {
        System.out.println("Loading " + filename);
    }

    @Override
    public void display() {
        System.out.println("Displaying " + filename);
    }
}
  1. Create the Proxy
public class ProxyImage implements Image {
    private RealImage realImage;
    private String filename;

    public ProxyImage(String filename) {
        this.filename = filename;
    }

    @Override
    public void display() {
        if (realImage == null) {
            realImage = new RealImage(filename);
        }
        realImage.display();
    }
}
  1. Client code to demonstrate the usage
public static void main(String[] args) {
        Image image1 = new ProxyImage("image1.jpg");
        Image image2 = new ProxyImage("image2.jpg");

        // Image will be loaded from disk
        image1.display();
        System.out.println();

        // Image will not be loaded from disk, already loaded
        image1.display();
        System.out.println();

        // Image will be loaded from disk
        image2.display();
    }