DEV Community

Cover image for Structural Design Patterns Decoded: Your Key to Agile and Robust Software
Suman Pal
Suman Pal

Posted on

Structural Design Patterns Decoded: Your Key to Agile and Robust Software

Structural  Design Patterns

Structural Design Pattern serves as a blueprint of how various objects and classes can be orchestrated to construct a larger and more complex framework, all with the aim of fulfilling multiple objectives simultaneously.


This article aims to present a detailed explanation of seven structural design patterns with respective UML and Sequence Diagrams.

Adapter Design Pattern :

Intent -  The Adapter Pattern allows objects with incompatible interfaces to work together. It acts as a bridge between two incompatible interfaces.

When to use

  • Use when you need to make existing classes work with others that have incompatible interfaces.
  • When integrating new components into an existing system without modifying existing code.

Key Points

  • Provides a wrapper to adapt one interface to another.
  • Useful for maintaining code flexibility and reusability.
  • Helps bridge the gap between different components or systems.

UML -
adapter

Code -
Imagine trying to charge an iPhone using an Android Charger, but we can't directly do that. so we are using an "ADAPTER" to convert Android to iPhone subsystem.

interface AndroidCharger {
    void chargerAndroidPhone();
}
interface AppleCharger {
    void chargeIPhone();
}
class SamsungCharger implements AndroidCharger {
    @Override
    public void chargerAndroidPhone() {
        System.out.println("Your phone is charging using SamsungCharger");
    }
}
class IPhoneCharger implements AppleCharger {
    @Override
    public void chargeIPhone() {
        System.out.println("your phone is charging using IPhoneCharger");
    }
}

class Iphone13 {
    private AppleCharger appleCharger;
    public Iphone13(AppleCharger appleCharger) {
        this.appleCharger = appleCharger;
    }
    public void chargeIphone() {
        appleCharger.chargeIPhone();
    }
}

class AndroidToAppleAdapterCharger implements AppleCharger {
    private AndroidCharger charger;
    public AndroidToAppleAdapterCharger(AndroidCharger charger) {
        this.charger = charger;
    }
    @Override
    public void chargeIPhone() {
        charger.chargerAndroidPhone();
        System.out.println("your phone is charging with adapter");
    }
}

public class Client {
    public static void main(String[] args) {
        /*charging iPhone with iPhone Charger scenario
        AppleCharger iphoneCharger = new IPhoneCharger();
        Iphone13 iphone13 = new Iphone13(iphoneCharger);
        iphone13.chargeIphone();*/

        AndroidCharger androidCharger = new SamsungCharger();
        AppleCharger androidToAppleAdapterCharger = new AndroidToAppleAdapterCharger(androidCharger);

        Iphone13 iphone13 = new Iphone13(androidToAppleAdapterCharger);
        iphone13.chargeIphone();
    }
}
Enter fullscreen mode Exit fullscreen mode

Proxy Design Pattern :

Intent
Proxy means ‘in place of’ or ‘on behalf of’ that directly explains Proxy Design Pattern.
The Proxy Pattern controls access to an object by acting as an intermediary. It can be used to add additional behavior or control access to a real object.

When to use
Use when you need to control access to an object, add additional behavior, or defer object creation.
When you want to manage resource-intensive operations efficiently.

Key Points
Can be applied to implement lazy loading, access control, logging, or monitoring.
Useful for optimizing performance and managing resources effectively.
Different types of proxies (e.g., virtual, remote) cater to various use cases.

UML -
proxy

Code -
A simple real-life scenario is that the college internet system employs a protection proxy that effectively restricts access to certain websites. It verifies the host being connected to and only allows access to the real internet if it is not on the restricted site list.

import java.util.ArrayList;
import java.util.List;

interface Internet{
    public void connectTo(String serverhost) throws Exception;
}

class RealInternet implements Internet{
    public void connectTo(String serverhost){
        System.out.println("Connecting to "+ serverhost);
    }
}

class ProxyInternet implements Internet{
    private Internet internet = new RealInternet();
    private static List<String> bannedSites;

    ProxyInternet(){
        bannedSites = new ArrayList<String>();
        bannedSites.add("whatsapp.com");
        bannedSites.add("facebook.com");
        bannedSites.add("instagram.com");
    }

    public void connectTo(String serverhost) throws Exception{
        if(bannedSites.contains(serverhost.toLowerCase())){
            throw new Exception("Access Denied");
        }
        internet.connectTo(serverhost);
    }
}

public class Client{
    public static void main (String[] args) {
        Internet internet = new ProxyInternet();
        try{
            internet.connectTo("google.com");
            internet.connectTo("facebook.com");
        }
        catch (Exception e){
            System.out.println(e.getMessage());
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Decorator Design Pattern :

Intent
The Decorator Pattern dynamically adds new responsibilities to objects without altering their class. It is used to extend the functionalities of objects at runtime.

When to use
Use when you want to add responsibilities to objects dynamically without altering their classes.
When you need to extend an object's behavior in a flexible and reusable way.

Key Points
Enables the open-closed principle, allowing you to add new functionality without modifying existing code.
Multiple decorators can be combined to create various combinations of behavior.
Useful for adding features such as logging, encryption, or formatting to existing classes.

UML -
decorator

Code -
A real-life scenario would be decorating or updating the features of a basic car into a Sports Car or Luxury Car, without modifying existing code.

interface Car {
    public void makeCar();
}

class SimpleCar implements Car {
    public void makeCar() {
        System.out.print("Simple Car.");
    }
}

class CarDecorator implements Car {
    protected Car car;

    public CarDecorator(Car car){
        this.car=car;
    }
    public void makeCar() {
        this.car.makeCar();
    }
}

class SportsCar extends CarDecorator {
    public SportsCar(Car car) {
        super(car);
    }
    public void makeCar(){
        super.makeCar();
        addNitro();
    }
    public void addNitro(){
        System.out.print(" Tranforming into a Sports Car.");
    }
}

class LuxuryCar extends CarDecorator {
    public LuxuryCar(Car c) {
        super(c);
    }
    public void makeCar(){
        super.makeCar();
        addComfort();
    }
    public void addComfort(){
        System.out.print(" Tranforming into a Luxury Car.");
    }
}

public class Client{

    public static void main(String[] args) {
        Car sportsCar = new SportsCar(new SimpleCar());
        sportsCar.makeCar();
        System.out.println("\n*****");

        Car sportsLuxuryCar = new SportsCar(new LuxuryCar(new SimpleCar()));
        sportsLuxuryCar.makeCar();
    }
}
Enter fullscreen mode Exit fullscreen mode

Interesting points:

The Adapter pattern presents a distinct interface for its subject, whereas the Proxy pattern maintains the original object's interface. In contrast, the Decorator pattern enhances the interface and adds supplementary functionality dynamically during runtime.


Composite Design Pattern :

Intent
The Composite Pattern composes objects into tree structures to represent part-whole hierarchies. Clients can treat individual objects and compositions of objects uniformly.

The Composite Pattern has three participants:

  1. Component– The component defines the interface for composition objects and facilitates access and management of its child components. It also incorporates default behavior for the interface that is applicable to all classes.
  2. Leaf– The leaf component establishes the behavior for basic objects within the composition, specifically representing the individual leaf objects present in the composition.
  3. Composite– The composite stores child components and implements operations related to the child components within the component interface.

When to use
Use when you need to represent part-whole hierarchies, and clients should treat individual objects and compositions uniformly.
When working with complex tree structures.

Key Points
Simplifies client code by treating leaf objects and composites the same way.
Supports recursive composition, enabling the creation of hierarchical structures.
Useful for creating complex structures like graphics, GUIs, or organizational hierarchies.

hierarchy

UML -
Composite

Code -
Let's take an example of the hierarchy of an organization.

import java.util.ArrayList;
import java.util.List;

interface Employee//Component 
{
    public void showEmployeeDetails();
}

class Tester implements Employee//Leaf  
{
    private String name;
    private long empId;
    private String position;

    public Tester(long empId, String name, String position)
    {
        this.empId = empId;
        this.name = name;
                this.position = position;
    }

    @Override
    public void showEmployeeDetails()
    {
        System.out.println(empId+" " +name);
    }
}

class Developer implements Employee//Leaf  
{
    private String name;
    private long empId;
    private String position;

    public Developer(long empId, String name, String position)
    {
        this.empId = empId;
        this.name = name;
                this.position = position;
    }

    @Override
    public void showEmployeeDetails()
    {
        System.out.println(empId+" " +name);
    }
}

class Manager implements Employee//Leaf  
{
    private String name;
    private long empId;
        private String position;

    public Manager(long empId, String name, String position)
    {
        this.empId = empId;
        this.name = name;
                this.position = position;
    }

    @Override
    public void showEmployeeDetails()
    {
        System.out.println(empId+" " +name);
    }
}

class CompanyDirectory implements Employee//Composite  
{
    private List<Employee> employeeList = new ArrayList<Employee>();

    @Override
    public void showEmployeeDetails()
    {
        for(Employee emp:employeeList)
        {
            emp.showEmployeeDetails();
        }
    }

    public void addEmployee(Employee emp)
    {
        employeeList.add(emp);
    }

    public void removeEmployee(Employee emp)
    {
        employeeList.remove(emp);
    }
}

public class Client   
{
    public static void main (String[] args)
    {
        Tester tester = new Tester(100, "David", "Tester");
        Developer developer = new Developer(101, "Steve", "Developer");
        CompanyDirectory engDirectory = new CompanyDirectory();
        engDirectory.addEmployee(tester);
        engDirectory.addEmployee(developer);

        Manager man1 = new Manager(200, "Tom", "Manager");
        Manager man2 = new Manager(201, "Robert", "Manager");

        CompanyDirectory managerDirectory = new CompanyDirectory();
        managerDirectory.addEmployee(man1);
        managerDirectory.addEmployee(man2);

        CompanyDirectory companyDirectory = new CompanyDirectory();
        companyDirectory.addEmployee(engDirectory);
        companyDirectory.addEmployee(managerDirectory);

        companyDirectory.showEmployeeDetails();
    }
}
Enter fullscreen mode Exit fullscreen mode

Fly Weight Design Pattern :

Intent
The Flyweight Pattern minimizes memory usage or computational expenses by sharing as much as possible with related objects. It is often used for managing a large number of similar objects efficiently.

When to use
Use when you have a large number of similar objects, and memory usage or computational expenses need to be minimized.
When managing objects with shared intrinsic properties and varying extrinsic properties.

Key Points
Shares common, immutable parts of objects to reduce memory overhead.
Useful for optimizing resource usage, especially in scenarios like graphical systems.
Separates intrinsic (shared) and extrinsic (unique) states for efficient memory management.

UML -
flyweight

Code -
Let's say we want to create a game map. And there is a forest present in a part of the game map. So we need to create lots of trees. so instead of creating an object for each tree, we will use a single object for Tree. Let's see how can we achieve that using the FlyWeight Pattern.

import java.util.HashMap;
import java.util.Random;

interface Tree {
    public void swingLeaves();
}

class OakTree implements Tree {
    private final String height;

    public OakTree(){
        height = "1.4m";
    }
    public void swingLeaves(){
        System.out.println("Leaves of Oak Tree are swinging");
    }
}

class PineTree implements Tree {
    private final String height;

    public PineTree(){
        height = "2.4m";
    }
    public void swingLeaves(){
        System.out.println("Leaves of Pine Tree are swinging");
    }
}

class BanyanTree implements Tree {
    private final String height;

    public BanyanTree(){
        height = "1.1m";
    }
    public void swingLeaves(){
        System.out.println("Leaves of Banyan Tree are swinging");
    }
}


class ForestFactory
{
    private static HashMap <String, Tree> forestMap = new HashMap<String, Tree>();

    public static Tree getTree(String type)
    {
        Tree tree = null;

        if (forestMap.containsKey(type))
                tree = forestMap.get(type);
        else
        {
            switch(type){
            case "OakTree":
                System.out.println("Oak Tree Created");
                tree = new OakTree();
                break;
            case "PineTree":
                System.out.println("Pine Tree Created");
                tree = new PineTree();
                break;
            case "BanyanTree":
                System.out.println("Banyan Tree Created");
                tree = new BanyanTree();
                break;
            default :
                System.out.println("Wrong Type!");
            }
            forestMap.put(type, tree);
        }
        return tree;
    }
}

public class Client{
    private static String[] treeType = {"OakTree", "PineTree", "BanyanTree"};

    public static void main(String args[]){
        //creating forest with 10trees
        for (int i = 0; i < 10; i++){
            Tree tree = ForestFactory.getTree(getRandTreeType());
            tree.swingLeaves();
        }
    }

    public static String getRandTreeType(){
        int randInt = new Random().nextInt(treeType.length);
        return treeType[randInt];
    }
}
Enter fullscreen mode Exit fullscreen mode

Facade Design Pattern :

Intent
The Facade Pattern provides a simplified interface to a complex subsystem. It acts as a high-level entry point for a set of interfaces in a system.

When to use
Use when you want to provide a simplified interface to a complex subsystem.
When you need to encapsulate intricate subsystems to improve code maintainability.

Key Points
Hides the complexity of a system from clients, providing a single, user-friendly entry point.
Improves code readability and maintainability by abstracting subsystem details.
Useful for simplifying interactions with external services, libraries, or complex modules.

UML -
Facade

Code -
Consider a hotel with multiple restaurants, including vegetarian, non-vegetarian, and mixed cuisine options. As a guest, you may desire access to various menus but lack knowledge of the available options. Fortunately, the hotel keeper serves as a knowledgeable intermediary, retrieving your desired menu items from the appropriate restaurant. Here, the hotel keeper acts as the facade, as he hides the complexities of the system hotel. Let’s see how it works

class Menu{
    private String menu;
    Menu(String menu){
        this.menu=menu;
    }
    public String getMenuDetails(){
        return this.menu;
    }
}

interface Hotel {
    public Menu getMenu();
}

class NonVegRestaurant implements Hotel {
    public Menu getMenu(){
        return new Menu("Chicken");
    }
}

class VegRestaurant implements Hotel {
    public Menu getMenu(){
        return new Menu("Paneer");
    }
}

class VegAndNonVegRestaurant implements Hotel {
    public Menu getMenu(){
        return new Menu("Mutton and Mushroom");
    }
}

interface HotelKeeper {
    public Menu getVegMenu();
    public Menu getNonVegMenu();
    public Menu getVegAndNonVegMenu();
}

class HotelKeeperImplementation implements HotelKeeper {
    public Menu getVegMenu(){
        VegRestaurant vegRest = new VegRestaurant();
        return vegRest.getMenu();
    }
    public Menu getNonVegMenu(){
        NonVegRestaurant nonVegRest = new NonVegRestaurant();
        return nonVegRest.getMenu();
    }
    public Menu getVegAndNonVegMenu(){
        VegAndNonVegRestaurant vegNonVegRest = new VegAndNonVegRestaurant();
        return vegNonVegRest.getMenu();
    }
}

public class Client{
    public static void main (String[] args) {
        HotelKeeper keeper = new HotelKeeperImplementation();

        System.out.println("Menu of Veg Restaurant : " + keeper.getVegMenu().getMenuDetails());
        System.out.println("Menu of NonVeg Restaurant : " + keeper.getNonVegMenu().getMenuDetails());
        System.out.println("Menu of Veg & NonVeg Restaurant : " + keeper.getVegAndNonVegMenu().getMenuDetails());
    }
}
Enter fullscreen mode Exit fullscreen mode

Bridge Design Pattern :

Intent
The Bridge Pattern separates an object's abstraction from its implementation, allowing them to vary independently.

When to use
Use when you want to separate the abstraction from its implementation, allowing them to vary independently.
When dealing with multiple dimensions of variation in your system.

Key Points
It enhances flexibility by decoupling the abstraction (interface) from the concrete implementation.
Useful when you have multiple dimensions of variations, like different platforms or rendering methods.
Promotes scalability as new abstractions or implementations can be added without affecting existing code.

UML -
bridge

Code -
We want to create a Device-remote system, where each device has a dedicated remote to access the device. So we want to segregate the Device and Remote from the implementation point of view. Let's see how can we achieve that using a Bridge Pattern.

//Right Side of the Bridge
interface Device{
    boolean deviceMode();
    void deviceOn();
    void deviceOff();
    void volumeUp();
    void volumeDown();
}

class TV implements Device{
    private boolean isTvOn = false;

    public boolean deviceMode(){
        return this.isTvOn;
    }
    public void deviceOn(){
        System.out.println("Tv is On");
        isTvOn = true;
    }
    public void deviceOff(){
        System.out.println("Tv is Off");
        isTvOn = false;
    }
    public void volumeUp(){
        System.out.println("Tv's volume is Up");
    }
    public void volumeDown(){
        System.out.println("Tv's volume is Down");
    }
}

class Radio implements Device{
    private boolean isRadioOn = false;

    public boolean deviceMode(){
        return this.isRadioOn;
    }
    public void deviceOn(){
        System.out.println("Radio is On");
        isRadioOn = true;
    }
    public void deviceOff(){
        System.out.println("Radio is Off");
        isRadioOn = false;
    }
    public void volumeUp(){
        System.out.println("Radio's volume is Up");
    }
    public void volumeDown(){
        System.out.println("Radio's volume is Down");
    }
}

//Left Side of the Bridge
abstract class Remote{
    public Device device;

    Remote(Device device){
        this.device = device;
    }

    public abstract void turnOnDevice();
    public abstract void turnOffDevice();
    public abstract void increaseDeviceVolume();
    public abstract void decreaseDeviceVolume();
}

class TvRemote extends Remote{
    TvRemote(Device device){
        super(device);
    }

    public void turnOnDevice(){
        device.deviceOn();
    }
    public void turnOffDevice(){
        device.deviceOff();
    }
    public void increaseDeviceVolume(){
        if(device.deviceMode())
            device.volumeUp();
        else
            System.out.println("Tv is Off. Can't increase volume. Please turn on TV first.");
    }
    public void decreaseDeviceVolume(){
        if(device.deviceMode())
            device.volumeDown();
        else
            System.out.println("Tv is Off. Can't decrease volume. Please turn on TV first.");
    }
}

class RadioRemote extends Remote{
    RadioRemote(Device device){
        super(device);
    }

    public void turnOnDevice(){
        device.deviceOn();
    }
    public void turnOffDevice(){
        device.deviceOff();
    }
    public void increaseDeviceVolume(){
        if(device.deviceMode())
            device.volumeUp();
        else
            System.out.println("Radio is Off. Can't increase volume. Please turn on Radio first.");
    }
    public void decreaseDeviceVolume(){
        if(device.deviceMode())
            device.volumeDown();
        else
            System.out.println("Radio is Off. Can't decrease volume. Please turn on Radio first.");
    }
}

public class Client
{
    public static void main(String[] args) {
        Device device = new TV();
        Remote remote = new TvRemote(device);

        remote.turnOnDevice();
        remote.increaseDeviceVolume();
        remote.decreaseDeviceVolume();
        remote.turnOffDevice();
        remote.increaseDeviceVolume();
    }
}
Enter fullscreen mode Exit fullscreen mode

In conclusion, structural design patterns provide reusable blueprints for organizing and composing classes and objects, making our code more adaptable, maintainable, and scalable. By employing these patterns, developers can simplify complex system architectures, bridge incompatibilities between components, and create hierarchical structures efficiently. Understanding and applying structural design patterns is a fundamental skill for building robust, flexible, and elegant software solutions. Happy coding!
I hope you found my blog enjoyable and informative.

Top comments (0)