DEV Community

Cover image for Design pattern (part 3): Structural patterns
Nguyễn Huy Hoàng
Nguyễn Huy Hoàng

Posted on • Edited on

Design pattern (part 3): Structural patterns

After the previous post about creational patterns, in this post, we will talk about structural patterns.

Creational design patterns are an essential part of software development. However, they are often overlooked and misunderstood by developers. In this blog post, we will explore why these patterns are crucial and how they can benefit your code.

The Importance of Structural Design Patterns

Many people don't have a clear understanding of what these patterns are or how they can benefit their code.

One reason is that many developers are not taught about these patterns in their education or training. Additionally, some developers may find the patterns too abstract or complicated to understand.

However, if used correctly, these patterns can make a big difference in your code.

What are Structural Design Patterns?

Structural design patterns are design patterns that help you to create a structure for your code. They are used to simplify the design by identifying the relationships between classes and objects.

Think about them like interpreters. They help simplify the relationship between the classes and objects while also making the structure of your code flexible and easy to understand.

Common structural patterns

1. Adapter

The name pretty much says it all. (phew, no metaphors needed!)

The adapter pattern is used to adapt one interface to another. This is useful when you have two incompatible components that you need to work together.

The main idea behind the adapter pattern has two parts:

  • Create an adapter interface that defines the methods that the client needs to use.

  • Create a concrete adapter class that implements the adapter interface. This adapter receives the input from the client, "converts" it to the format that the other component needs, and then passes it to the other component.

Note that the client can also use the concrete adapter class directly. However, this is not recommended because it will make the code harder to maintain. Moreover, using the adapter interface will make it easier to change the concrete adapter class in the future.

Example:

public interface XMLToJSONAdapter {
  void processXML(String xml);
}

public class XMLToJSONAdapterImpl implements XMLToJSONAdapter {

    private JSONProcessor jsonProcessor;

    @Override
    public void processXML(String xml) {
        String json = convertXMLToJSON(xml);
        jsonProcessor.processJSON(json);
    }

    private String convertXMLToJSON(String xml) {
        // convert XML to JSON
        return json;
    }
}

public class Client {

    private XMLToJSONAdapter adapter;

    public void processXML(String xml) {
        adapter.processXML(xml);
    }
}

public class Main {
    public static void main(String[] args) {
        JSONProcessor jsonProcessor = new JSONProcessorImpl();
        XMLToJSONAdapter adapter = new XMLToJSONAdapterImpl(jsonProcessor);
        Client client = new Client(adapter);
        client.processXML(xml);
    }
}
Enter fullscreen mode Exit fullscreen mode

2. Bridge

Imagine you have a TV and a remote control.

The remote control can be used to control your TV. However, the remote control is not compatible with other TVs. This means that if you want to use the remote control on another TV, you will need to buy a new remote control.

One day, your friend gave you a universal remote control and told you that it can be used on any TV. You tested it on your TV and it worked perfectly. When you used it on other TVs, you realized that the remote control is also compatible with other TVs, regardless of the brand.

The question is: how can the remote control be able to control other TVs without knowing the internal structure of the TV?

The answer is: by using the bridge pattern.

The main idea behind the bridge pattern is to control the TV using an interface. This way, we will delegate the actions to the TV itself. The remote control only acts as a controller, transmitting the commands from the user to the TV.

The implementation of the bridge pattern has these parts:

  • Create an interface that defines the methods of the object that will be controlled.

  • Create a concrete class that implements the interface. This class will execute the commands from the controller.

  • Create a controller class that contains an instance of the concrete class. This class will receive the commands from the user and pass them to the concrete class. Thus, this controller class acts as a bridge between the user and the concrete class.

Example:

public interface TV {
    void on();
    void off();
    void setChannel(int channel);
}
public class SonyTV implements TV {
    @Override
    public void on() {
        System.out.println("Sony TV is on");
    }

    @Override
    public void off() {
        System.out.println("Sony TV is off");
    }

    @Override
    public void setChannel(int channel) {
        System.out.println("Sony TV channel is set to " + channel);
    }
}
public class RemoteControl {
    private TV tv;

    public RemoteControl(TV tv) {
        this.tv = tv;
    }

    public void on() {
        tv.on();
    }

    public void off() {
        tv.off();
    }

    public void setChannel(int channel) {
        tv.setChannel(channel);
    }
}

public class Main {
    public static void main(String[] args) {
        TV tv = new SonyTV();
        RemoteControl remoteControl = new RemoteControl(tv);
        remoteControl.on();
        remoteControl.setChannel(5);
        remoteControl.off();
    }
}
Enter fullscreen mode Exit fullscreen mode

3. Composite

Imagine you have a box.

The box can contain other boxes. It can also contain other items such as books, pens, and so on.

The question is: how can you calculate the total cost of the box?

The first obvious solution is to unbox the box and calculate the cost of each item. However, this is not a good solution because it will require knowing the internal structure of the box.

The solution? Use the composite pattern.

The main idea behind the composite pattern is to delegate the calculation of the total cost to the box itself. The box will go through all the items in the box and calculate the total cost. If one of the items is a box, it will also go through the items in that box and calculate the total cost, and so on.

The greatest benefit of using the composite pattern is that you don't need to know the internal structure of the box. Moreover, each box can have its implementation of the calculation of the total cost, such as adding a discount, tax or shipping cost.

Example:

public interface Item {
    int getPrice();
}
public class Book implements Item {
    private int price;

    public Book(int price) {
        this.price = price;
    }

    @Override
    public int getPrice() {
        return price;
    }
}
public class Box implements Item {
    private List<Item> items;

    public Box(List<Item> items) {
        this.items = items;
    }

    @Override
    public int getPrice() {
        int total = 0;
        for (Item item : items) {
            total += item.getPrice();
        }
        return total;
    }
}

public class Main {
    public static void main(String[] args) {
        Item book1 = new Book(10);
        Item book2 = new Book(20);
        Item book3 = new Book(30);
        Item box1 = new Box(Arrays.asList(book1, book2));
        Item box2 = new Box(Arrays.asList(book3, box1));
        System.out.println(box2.getPrice()); // 80
    }
}
Enter fullscreen mode Exit fullscreen mode

4. Decorator

This name of the pattern is a little misleading.

One other name for this pattern is the wrapper pattern. Instead of modifying the original class, the decorator pattern creates a wrapper class that contains the original class.

The main idea behind the decorator pattern is to add new functionality to the original class without modifying it. This is useful when you want to add new functionality to the original class without changing the original class.

However, what fascinates me the most about this pattern is that it can be used on top of each other. This means that you can add multiple decorators to the original class, therefore adding a "chain" of functionality to the original class.

Example:

public interface Processor {
    void process(String message);
}
public class Decorator implements Processor {
    private Processor processor;

    public Decorator(Processor processor) {
        this.processor = processor;
    }

    @Override
    public void process(String message) {
        processor.process(message);
    }
}
public class Stage1Decorator implements Decorator {
    @Override
    public void process(String message) {
      super.process(message);
      process1(message);
    }
}
public class Stage2Decorator implements Decorator {
    @Override
    public void process(String message) {
      super.process(message);
      process2(message);
    }
}
public class Main {
    public static void main(String[] args) {
        Processor processor = new Stage1Decorator();
        processor = new Stage2Decorator(processor);
        processor.process("Hello World");
    }
}
Enter fullscreen mode Exit fullscreen mode

5. Facade

Again, the name of the pattern is pretty self-explanatory (finally, something that the programmers named well!).

The facade pattern is used to provide a simplified interface to a complex system. This is useful when you have a complex system that you want to hide the complexity from the client.

I believe that the facade pattern is one of the most used patterns. You may not know it by name, but you have probably used it before. For example, a utility class that contains a bunch of static methods that you used to simplify your code.

Example:

public class Facade {
    public void process() {
        Stage1 stage1 = new Stage1();
        stage1.process();
        Stage2 stage2 = new Stage2();
        stage2.process();
        Stage3 stage3 = new Stage3();
        stage3.process();
    }
}
public class Main {
    public static void main(String[] args) {
        Facade facade = new Facade();
        facade.process();
    }
}
Enter fullscreen mode Exit fullscreen mode

6. Flyweight

Imagine you have a vacuum cleaner.

In your house, you have a lot of surfaces that need to be cleaned. However, each type of surface has a different cleaning method. For example, you have a wooden floor, a carpet, a marble floor, and so on.

The question is: how can you clean all the surfaces in your house using the same vacuum cleaner?

The solution? You use different attachments for the vacuum cleaner. For example, you can use a wide brush for the carpet, a small brush for the wooden floor, and so on.

The main idea behind the flyweight pattern is to extract the common parts of the objects and store them in a separate object. This way, you can use the same object for multiple purposes.

Example:

public class Attachment {
    private String name;

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

    public String getName() {
        return name;
    }
}
public class AttachmentFactory {
    private static final Map<String, Attachment> attachments = new HashMap<>();

    public static Attachment getAttachment(String name) {
        Attachment attachment = attachments.get(name);
        if (attachment == null) {
            attachment = new Attachment(name);
            attachments.put(name, attachment);
        }
        return attachment;
    }
}
public class VacuumCleaner {
    private Attachment attachment;

    public void setAttachment(Attachment attachment) {
        this.attachment = attachment;
    }

    public void clean() {
        System.out.println("Cleaning with " + attachment.getName());
    }
}

public class Main {
    public static void main(String[] args) {
        VacuumCleaner vacuumCleaner = new VacuumCleaner();
        vacuumCleaner.setAttachment(AttachmentFactory.getAttachment("brush"));
        vacuumCleaner.clean();
        vacuumCleaner.setAttachment(AttachmentFactory.getAttachment("wide brush"));
        vacuumCleaner.clean();
    }
}
Enter fullscreen mode Exit fullscreen mode

7. Proxy

Those of you who used a VPN before know what the proxy pattern is.

The main idea behind the proxy pattern is to provide a placeholder for another object to control access to it.

You can think of it as having an assistant that does all the work for you. The assistant will control who can reach out to you, and also handle all the requests that you receive.

The proxy pattern has many uses:

  • It can be used to provide a placeholder for a remote object.

  • It can be used to hide the complexity of the original object.

  • It can be used to add new functionality to the original object.

  • It can be used to control access to the original object.

The implementation of the proxy pattern is very similar to the implementation of the facade pattern, with several parts:

  • The interface defines the functionality of the original object.

  • Both the original object and the proxy object implement the interface.

  • The proxy object contains a reference to the original object.

  • When the client calls a method on the proxy object, the proxy object calls the same method on the original object, with additional functionality.

Example:

public interface Image {
    void display();
}
public class RealImage implements Image {
    private String filename;

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

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

    @Override
    public void display() {
        System.out.println("Displaying " + filename);
    }
}
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();
    }
}
public class Main {
    public static void main(String[] args) {
        Image image = new ProxyImage("test_10mb.jpg");
        image.display();
        System.out.println("");
        image.display();
    }
}
Enter fullscreen mode Exit fullscreen mode

That is all for this post. I hope you enjoyed it, and I hope that you learned something new.

A good way in my opinion to learn more about design patterns is to try to explain them in your own words. This way, you will be able to understand the idea behind the pattern, and you will be able to use it in your projects.

Remember that this is only a brief introduction to the design patterns. The details of each pattern are much more complex, and you should read more about them if you want to use them in your projects. I will also write more posts about design patterns in the future.

Resources

Top comments (0)