Java Structural Design Patterns

Structural Design Patterns in Java

Structural design patterns deal with object composition and relationships, allowing us to compose objects to form larger structures and relationships between them. In this discussion, we’ll cover four structural patterns: Adapter, Decorator, Proxy, and Composite.

1. Adapter Pattern

The Adapter pattern allows incompatible interfaces to work together. It acts as a bridge between two incompatible interfaces by wrapping an existing class with a new interface.

Implementation

  • Example: Adapting a MediaPlayer interface to a new AdvancedMediaPlayer interface.
Java
// Target interface
public interface MediaPlayer {
    void play(String audioType, String fileName);
}

// Adaptee interface
public interface AdvancedMediaPlayer {
    void playVlc(String fileName);
    void playMp4(String fileName);
}

// Concrete Adaptee class
public class VlcPlayer implements AdvancedMediaPlayer {
    @Override
    public void playVlc(String fileName) {
        System.out.println("Playing vlc file. Name: " + fileName);
    }

    @Override
    public void playMp4(String fileName) {
        // Do nothing
    }
}

// Concrete Adaptee class
public class Mp4Player implements AdvancedMediaPlayer {
    @Override
    public void playVlc(String fileName) {
        // Do nothing
    }

    @Override
    public void playMp4(String fileName) {
        System.out.println("Playing mp4 file. Name: " + fileName);
    }
}

// Adapter class
public class MediaAdapter implements MediaPlayer {
    AdvancedMediaPlayer advancedMusicPlayer;

    public MediaAdapter(String audioType) {
        if (audioType.equalsIgnoreCase("vlc")) {
            advancedMusicPlayer = new VlcPlayer();
        } else if (audioType.equalsIgnoreCase("mp4")) {
            advancedMusicPlayer = new Mp4Player();
        }
    }

    @Override
    public void play(String audioType, String fileName) {
        if (audioType.equalsIgnoreCase("vlc")) {
            advancedMusicPlayer.playVlc(fileName);
        } else if (audioType.equalsIgnoreCase("mp4")) {
            advancedMusicPlayer.playMp4(fileName);
        }
    }
}

// Concrete Target class
public class AudioPlayer implements MediaPlayer {
    MediaAdapter mediaAdapter;

    @Override
    public void play(String audioType, String fileName) {
        if (audioType.equalsIgnoreCase("mp3")) {
            System.out.println("Playing mp3 file. Name: " + fileName);
        } else if (audioType.equalsIgnoreCase("vlc") || audioType.equalsIgnoreCase("mp4")) {
            mediaAdapter = new MediaAdapter(audioType);
            mediaAdapter.play(audioType, fileName);
        } else {
            System.out.println("Invalid media. " + audioType + " format not supported");
        }
    }
}

// Client
public class AdapterPatternDemo {
    public static void main(String[] args) {
        AudioPlayer audioPlayer = new AudioPlayer();

        audioPlayer.play("mp3", "beyond the horizon.mp3");
        audioPlayer.play("mp4", "alone.mp4");
        audioPlayer.play("vlc", "far far away.vlc");
        audioPlayer.play("avi", "mind me.avi");
    }
}

2. Decorator Pattern

The Decorator pattern allows behavior to be added to individual objects, dynamically, without affecting the behavior of other objects from the same class. It is useful for adhering to the Single Responsibility Principle by allowing functionality to be divided between classes with unique areas of concern.

Implementation

  • Example: Adding features to a simple Text object.
Java
// Component interface
public interface Text {
    String getText();
}

// Concrete Component class
public class SimpleText implements Text {
    private String text;

    public SimpleText(String text) {
        this.text = text;
    }

    @Override
    public String getText() {
        return text;
    }
}

// Decorator class
public abstract class TextDecorator implements Text {
    protected Text decoratedText;

    public TextDecorator(Text decoratedText) {
        this.decoratedText = decoratedText;
    }

    @Override
    public String getText() {
        return decoratedText.getText();
    }
}

// Concrete Decorator class
public class BoldTextDecorator extends TextDecorator {
    public BoldTextDecorator(Text decoratedText) {
        super(decoratedText);
    }

    @Override
    public String getText() {
        return "<b>" + super.getText() + "</b>";
    }
}

// Concrete Decorator class
public class ItalicTextDecorator extends TextDecorator {
    public ItalicTextDecorator(Text decoratedText) {
        super(decoratedText);
    }

    @Override
    public String getText() {
        return "<i>" + super.getText() + "</i>";
    }
}

// Client
public class DecoratorPatternDemo {
    public static void main(String[] args) {
        Text simpleText = new SimpleText("Hello, World!");

        Text boldText = new BoldTextDecorator(simpleText);
        System.out.println(boldText.getText());

        Text italicText = new ItalicTextDecorator(simpleText);
        System.out.println(italicText.getText());

        Text boldItalicText = new BoldTextDecorator(new ItalicTextDecorator(simpleText));
        System.out.println(boldItalicText.getText());
    }
}

3. Proxy Pattern

The Proxy pattern provides a surrogate or placeholder for another object to control access to it. Proxies are used to control access to the original object, providing additional functionality like lazy initialization, access control, logging, etc.

Implementation

  • Example: Creating a proxy for accessing an image file.
Java
// Subject interface
public interface Image {
    void display();
}

// RealSubject class
public class RealImage implements Image {
    private String fileName;

    public RealImage(String fileName) {
        this.fileName = fileName;
        loadFromDisk();
    }

    private void loadFromDisk() {
        System.out.println("Loading " + fileName);
    }

    @Override
    public void display() {
        System.out.println("Displaying " + fileName);
    }
}

// Proxy class
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();
    }
}

// Client
public class ProxyPatternDemo {
    public static void main(String[] args) {
        Image image = new ProxyImage("test.jpg");

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

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

4. Composite Pattern

The Composite pattern allows you to compose objects into tree structures to represent part-whole hierarchies. It lets clients treat individual objects and compositions of objects uniformly.

Implementation

  • Example: Implementing a file system with files and directories.
Java
import java.util.ArrayList;
import java.util.List;

// Component interface
public interface FileSystemComponent {
    void showDetails();
}

// Leaf class
public class File implements FileSystemComponent {
    private String name;

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

    @Override
    public void showDetails() {
        System.out.println("File: " + name);
    }
}

// Composite class
public 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();
        }
    }
}

// Client
public class CompositePatternDemo {
    public static void main(String[] args) {
        File file1 = new File("file1.txt");
        File file2 = new File("file2.txt");
        File file3 = new File("file3.txt");

        Directory directory1 = new Directory("dir1");
        Directory directory2 = new Directory("dir2");

        directory1.addComponent(file1);
        directory1.addComponent(file2);
        directory2.addComponent(file3);

        Directory rootDirectory = new Directory("root");
        rootDirectory.addComponent(directory1);
        rootDirectory.addComponent(directory2);

        rootDirectory.showDetails();
    }
}

Summary

Structural design patterns help in building large, flexible, and maintainable structures by composing objects. They focus on how objects are composed to form larger structures and simplify the design by identifying relationships between entities.

  1. Adapter Pattern: Allows incompatible interfaces to work together.
  2. Decorator Pattern: Adds behavior to objects dynamically.
  3. Proxy Pattern: Controls access to an object, providing additional functionality.
  4. Composite Pattern: Composes objects into tree structures to represent part-whole hierarchies.

Understanding and implementing these patterns can significantly improve the flexibility and maintainability of your code.

Scroll to Top