Top Structural Design Patterns With Real Examples In Java

Amirhosein Gharaati
Javarevisited
Published in
10 min readSep 26, 2023

We can apply solutions to commonly occurring problems by knowing design patterns in software design.

Structural design patterns explain how to assemble objects and classes into larger structures, while keeping these structures flexible and efficient.

Table of Contents

If you have already read “Creational Patterns” article, jump directly to Structural Patterns section.

Design patterns

Design patterns are solutions to commonly occurring problems in software design. They are like blueprints that you can customize to solve a recurring design problem in your code.

The pattern is not a specific piece of code, but a general concept that you can apply it in your software or customize them and use your own.

Why should we learn design patterns?

There are couple reasons to learn design patterns:

  1. They are tried and tested solutions: and even if you don’t encounter with those problems, knowing patterns teach you how to solve them in object oriented design.
  2. They define a common language that you and your teammates can use to communicate with each other more efficiently. When you say “Factory”, maybe everyone understands the idea that you are talking about.

What does the pattern consist of?

The sections that are usually present in a pattern description are:

  • Intent: describes both the problem and the solution.
  • Motivation: further explains the problem and the solution the pattern makes possible.
  • Structure: shows each part of the pattern and how they are related.
  • Code example: in a programming language to show the solution.

But for simplicity, we summarize these parts.

Classification of Patterns

There are three main groups of design patterns in software development:

  • Creational patterns: provide object creation mechanisms that increase flexibility and reuse of existing code.
  • Structural patterns: explain how to assemble objects and classes into larger structures, while keeping these structures flexible and efficient.
  • Behavioral patterns: take care of effective communication and the assignment of responsibilities between objects.

In this article, we only talk about popular structural patterns.

Structural patterns

We try to assemble different classes and objects to make a larger structure while keeping them flexible.

You can see the implementations in this repository:

https://github.com/AmirHosein-Gharaati/design-patterns

Adapter

Allows objects with incompatible interfaces to collaborate.

Suppose you have a Retail or E-commerce system that has different payment gateways. Each gateway has its implementation and APIs, and you want to integrate them into your system: GatewayA and GatewayB. Your system may only have one interface, so you should use that one and implement each separately.

You have an interface for your first payment:
PaymentGateway

public interface PaymentGateway {
void processPayment(double amount);
}

And you implement your first gateway:
GatewayA

public class GatewayA implements PaymentGateway {
@Override
public void processPayment(double amount) {
System.out.println("Processing payment with gateway A: $" + amount);
}
}

Now imagine you want to add another gateway:
GatewayB

public class GatewayB {
public void charge(double amount) {
System.out.println("Charging payment with GatewayB: $" + amount);
}
}

Which the second gateway has different implementation and process rather than first one.

You want to make your second gateway work with the old interface you defined before. In this case, you create an adapter for GatewayB which implements the interface:
GatewayBAdapter

@AllArgsConstructor
public class GatewayBAdapter implements PaymentGateway {
private GatewayB gatewayB;

@Override
public void processPayment(double amount) {
gatewayB.charge(amount);
}
}

Within the interface you override, you have your implementation to work with old interface.

Let’s see the usage:
Main

public class Main {
public static void main(String[] args) {
PaymentGateway gateway1 = new GatewayA();
PaymentGateway gateway2 = new GatewayBAdapter(new GatewayB());

double amount = 100.0;

gateway1.processPayment(amount);
gateway2.processPayment(amount);
}
}

The output is:

Processing payment with gateway A: $100.0
Charging payment with GatewayB: $100.0

Applicability

Use adapter design pattern when:

  • You want to use some existing class, but its interface isn’t compatible with the rest of your code.
  • You want to reuse several existing subclasses that lack some common functionality that can’t be added to the superclass.

Pros

  • You can separate the interface or data conversion code from the primary business logic of the program.
  • You can introduce new types of adapters into the program without breaking the existing client code.

Cons

  • The overall complexity of the code increases because you need to introduce a set of new interfaces and classes.

Read more about adapter design pattern:

Facade

Provides a simplified interface to a library, a framework, or any other complex set of classes.

One of the real-world examples of this pattern is JDBC. When you try to connect to an SQL-based database, you only provide the protocol, username, and password (or maybe something more).

But we are not aware of complexity and we don’t care.

Let’s go through another example.

Assume you have some “Report” generators in your system that can generate different outputs. You just want to pass data and generate the report. That’s all.

Let’s say we want to generate a report for some Products

We use an interface for our reporters named:
Reporter

public interface Reporter {
void generateReport(ReportData rejectedProducts);
}

Two report generators implement this interface
CsvReporter

@NoArgsConstructor
public class CsvReporter implements Reporter {
@Override
public void generateReport(ReportData rejectedProducts) {
System.out.println("Generating report in CSV format");
}
}

ExcelReporter

@NoArgsConstructor
public class ExcelReporter implements Reporter {
@Override
public void generateReport(ReportData rejectedProducts) {
System.out.println("Generating report in Excel format");
}
}

Now we create our facade
ReportFacade

public class ReportFacade {
private final Reporter excelReporter;
private final Reporter csvReporter;

public ReportFacade(Reporter excelReporter, Reporter csvReporter) {
this.excelReporter = excelReporter;
this.csvReporter = csvReporter;
}
public void generateExcelReport(ReportData reportData) {
excelReporter.generateReport(reportData);
}
public void generateCsvReport(ReportData reportData) {
csvReporter.generateReport(reportData);
}
}

ReportData

@AllArgsConstructor
public class ReportData {
private List<Product> badProducts;
}

Let’s use our facade:

Main

public class Main {
public static void main(String[] args) {
List<Product> badProducts = List.of(
new Product(1L, "One", 100),
new Product(2L, "Two", 200),
new Product(3L, "Three", 300)
);
ReportData reportData = new ReportData(badProducts);
Reporter csvReporter = new CsvReporter();
Reporter excelReporter = new ExcelReporter();

ReportFacade facade = new ReportFacade(excelReporter, csvReporter);
facade.generateCsvReport(reportData);
facade.generateExcelReport(reportData);
}
}

Note: The idea of facade is just you reduce complexity at high level. You can implement anything that fits yours best.

Applicability

Use facade when:

  • You need to have a limited but straightforward interface to a complex subsystem.
  • You want to structure a subsystem into layers.

Read more about facade pattern:

Composite

Lets you compose objects into tree structures and then work with these structures as if they were individual objects.

In this pattern, we are building something like a tree, because we are composing different objects together.

Refactoring Guru Composite Pattern

One of the examples we have seen in our computers is file system components. There are directories and files. Each directory can contain some directories and files.

So we have two kinds of classes: Leaf class, Composite class

In this example, File is a leaf class and Directory is a composite class. Let’s implement it in Java.

I just want to display the hierarchy for a tree of files and directories with indentation. First, create an interface named:
FileSystemComponent

public interface FileSystemComponent {
void display(int baseIndent);
}

The reason why I want to pass a base indentation value is just I want to print the lines with indentation.

Then create a class for your leaf class and it should implement the interface:
MyFile

@AllArgsConstructor
@Getter
public class MyFile implements FileSystemComponent {
private final String name;

@Override
public void display(int baseIndent) {
String line = " ".repeat(baseIndent) + name;
System.out.println(line);
}
}

Now create you composite class and again implement the interface. In the composite you should contain a list of instances of interface:
MyDirectory

@Getter
public class MyDirectory implements FileSystemComponent {
private final String name;
private final List<FileSystemComponent> components;
private static final int NUMBER_OF_INDENT = 3;

public MyDirectory(String name) {
this.name = name;
this.components = new ArrayList<>();
}

@Override
public void display(int baseIndent) {
String line = " ".repeat(baseIndent) + name;
System.out.println(line);

components.forEach(component ->
component.display(baseIndent + NUMBER_OF_INDENT));
}

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

I just want to use indentations for showing the hierarchy of files. You can do whatever you want.

Let’s test our application:
Main

public class Main {
public static void main(String[] args) {
MyFile document1 = new MyFile("Document1.txt");
MyFile document2 = new MyFile("Document2.txt");
MyFile image = new MyFile("Image.jpg");

MyDirectory rootDirectory = new MyDirectory("Root");
MyDirectory documentsDirectory = new MyDirectory("Documents");

rootDirectory.addComponent(documentsDirectory);
rootDirectory.addComponent(image);
documentsDirectory.addComponent(document1);
documentsDirectory.addComponent(document2);
rootDirectory.display(0);
}
}

Output:

Root
Documents
Document1.txt
Document2.txt
Image.jpg

Applicability

Use this pattern when:

  • You have to implement a tree-like object structure.
  • You want the client code to treat both simple and complex elements uniformly.

Pros

  • You can work with complex tree structures more conveniently.
  • You can introduce new element types into the app without breaking the existing code, which now works with the object tree. (Open/Closed Principle)

Cons

  • It might be difficult to provide a common interface for classes whose functionality differs too much.

Read more about composite pattern:

More Structural Patterns

Bridge

The bridge design pattern is used to decouple a class into two parts — abstraction and its implementation — so that both can evolve in the future without affecting each other.

Suppose you want to build a file downloader application which it should download and store files on any operating system.

  • You should be able to add more platform support in the future with minimum changes.
  • Additionally, If you want to add more support in the downloader class (e.g. delete the download in Windows only), then It should not affect the client code as well as the Linux downloader.

In this case, you break down the downloader component into abstraction and implementer parts.

I found this article useful about it. So you can have a look:

Decorator

Lets you attach new behaviors to objects by placing these objects inside special wrapper objects that contain the behaviors.

You have a reporter system in your application. You want to add more behaviors to it, but the point is you don’t want to break the previous implementation meanwhile having the new feature.

In this case, you create an abstract class with the implementation of your interface. Then using a concrete class extending the abstract, you add more functionalities besides the previous implementation.

You can see a simple example here:

Flyweight

Lets you fit more objects into the available amount of RAM by sharing common parts of state between multiple objects instead of keeping all of the data in each object.

So in this pattern, you are going to have shared objects. You don’t want to create objects with the same parameters again since the functionality is the same and maybe you are going to control the memory consumption by a large number of objects.

This pattern is also related to the Factory pattern. Assume that you have a map of key values. You assign a key to an object and then store it as a value in the map. When you want to get an object, you use that map to see if that object is already instantiated or not.

See this example in action:

Proxy

Lets you provide a substitute or placeholder for another object. A proxy controls access to the original object, allowing you to perform something either before or after the request gets through to the original object.

This also is one of my favorite patterns, but it is not really popular in Java.

1.You may have a service that is very crucial and you need to log everything that is operated within it. So you may force the developers to use another class (called proxy) to use. Here is quick example:

CrucialService

package com.mycompany.patterns.proxy.service;

class CrucialService {
public void operate() {
System.out.println("CrucialService operation");
}
}

Note: it is a package-private class

CrucialProxy

package com.mycompany.patterns.proxy.service;

import lombok.RequiredArgsConstructor;

import java.util.logging.Logger;

@RequiredArgsConstructor
public class CrucialProxy {
private final CrucialService service;
private final Logger log;

public CrucialProxy() {
this.service = new CrucialService();
this.log = Logger.getLogger(CrucialProxy.class.getName());
}

public void operate(String name) {
log.info("Some operation has been started with name: %s".formatted(name));
service.operate();
}
}

Main

package com.mycompany.patterns.proxy;

import com.mycompany.patterns.proxy.service.CrucialProxy;

public class Main {
public static void main(String[] args) {
CrucialProxy crucialProxy = new CrucialProxy();
crucialProxy.operate("Proxy Pattern");
}
}

Output:

Sep 24, 2023 8:28:12 PM com.mycompany.patterns.proxy.service.CrucialProxy operate
INFO: Some operation has been started with name: Proxy Pattern
CrucialService operation

2.A real-world example is: Internet access is guarded behind a network proxy. All network requests go through a proxy which first checks the requests for allowed websites and posted data to the network. If the request looks suspicious, the proxy blocks the request — otherwise request passes through. (consider it as protection proxy)

3.You have a massive object that consumes a vast amount of system resources. You need it from time to time, but not always.

The Proxy pattern suggests that you create a new proxy class with the same interface as an original service object. Then you update your app so that it passes the proxy object to all of the original object’s clients.

Now if you need to execute something either before or after the primary logic of the class, the proxy lets you do this without changing that class. (Lazy initialization or virtual proxy)

Proxy pattern can be used in these tasks:

  • Logging
  • Catching exceptions
  • Caching
  • Dispatching events before or after some method is called
  • Access validation

Read more about the proxy pattern:

Conclusion

There are different groups of design patterns in software design which we talked about “Structural patterns”.

Choosing the proper pattern, can make our project more flexible and reusable. We should only get the idea behind a pattern, and apply it to our software.

--

--