In this tutorial, we will show the real-world problem and solve it with a bridge pattern.

Real-world business problem that I had in my real job

Our client is uploading his files into the AWS and the task of our application was to check for new files every hour. If it finds any new file, we need to process that file in our application. The only tricky part is that he wants to upload to different platforms: AWS, GCP, Azure, and even on some remote servers that are not on the cloud. Also, files are not always in the same format. Sometimes, the client uploads an Excel file, CSV, some custom format, etc. The last thing is the content of the file is different. He can upload a list of books, a list of publishers, etc.

So, a few possible use cases might look like this:

  • books in CSV format uploaded into the AWS
  • Authors are uploaded into the GCP in Excel format, etc.

Expectations were to cover all possible combinations.

How do we recognize this is the bridge pattern?

Parts are moving around three axes:

  • Type of storage(AWS, GCP, etc.)
  • Format(CSV, Excel, etc.)
  • Content(Book, Publisher)

Every one of those axes is one hierarchy. Reader hierarchy is tasked with reading files for you using InputStream read(String location). You give the location of the file to the reader and the output is InputStream. Take note that the result is not Book or Publisher. The Reader is not capable of parsing things from the file. It just reads for you and returns you an InputStream as a result. The next step is to pass that InputStream to the Parser. The Parser can parse content from the InputStream and return you a Book.

The only last problem is that even Parser has too two responsibilities:

  • Parse specific format
  • Parse specific content.

That is coupling two of our axes and it’s not a good idea. When you see that you have moving parts that vary along two or more axes and every axe is one hierarchy and axes need to work together, a bridge pattern is the solution.

UML diagram

Let’s take a look at the UML diagram before coding:

Simple diagram. We have three hierarchies: Reader, Parser, and Content. The Reader does its job(reading from location and returning InputStream and delegated parsing to the Parser. Parser does the format parsing but delegates content-specific things to the Content)

Coding time

Let’s start with the reader:

public interface Reader {
    InputStream read(String location);
}

public class AWSReader implements Reader {
    @Override
    public InputStream read(String location) {
        //Handling auth and reading from aws s3 using aws libraries
    }
}

public class GCPReader implements Reader {
    @Override
    public InputStream read(String location) {
        //Handling auth and reading from Google Cloud Storage using GCP libraries
    }
}

I didn’t show the real implementation code using AWS or GCP libraries since that’s off-topic. What is important to realize is that handling everything related to the AWS(reading from AWS but also doing authorization or authentication if needed) is inside the AWSReader. You won’t repeat that AWS code anywhere else. The same applies to GCP. The next part of our bridge pattern is the Parser:

public interface Parser {
    List<Content> parse(InputStream inputStream, String fileName);
}

public class ExcelParser implements Parser {
    private final ContentFactory factory;

    public ExcelParser(ContentFactory factory) {
        this.factory = factory;
    }

    @Override
    public List<Content> parse(InputStream inputStream, String fileName) {
        List<Content> parsedObjects = new ArrayList<>();
        try {
            Workbook wb = WorkbookFactory.create(inputStream);
            Sheet sheet = wb.getSheetAt(0);
            Row row;
            Cell cell;

            int rows; // No of rows
            rows = sheet.getPhysicalNumberOfRows();

            int cols = 0; // No of columns
            int tmp = 0;

            // This trick ensures that we get the data properly even if it doesn't start from the first few rows
            for(int i = 0; i < 10 || i < rows; i++) {
                row = sheet.getRow(i);
                if(row != null) {
                    tmp = sheet.getRow(i).getPhysicalNumberOfCells();
                    if(tmp > cols) cols = tmp;
                }
            }

            for(int r = 0; r < rows; r++) {
                Content obj = factory.create(fileName);
                row = sheet.getRow(r);
                if(row != null) {
                    for(int c = 0; c < cols; c++) {
                        cell = row.getCell((short)c);
                        if(cell != null) {
                            cell.setCellType(CellType.STRING);

                            obj.setField(
                                    obj.getExcelMetadata().get(c).getSetterFunction(),
                                    obj.getExcelMetadata().get(c).getReadingFunction().apply(cell.getStringCellValue())
                            );
                        }
                    }
                    parsedObjects.add(obj);
                }
            }
        } catch(Exception ioe) {
            ioe.printStackTrace();//we should handle exception properly here
        }
        return parsedObjects;
    }
}

This one is a bit more complicated and requires explanation. You may need to go through this code a few times to understand it but when you do understand it, it will fit perfectly. First of all, take a look at the signature of the parse method in the Parser interface. fileName is an input parameter because content is determined based on the name of the file. For example, if the name of the file contains *book* then the content of the file is a list of books.

Parser does too much!

The next thing to realize is that this Parser handles both format and content. We said earlier that those two things are different responsibilities and that those should be decoupled and we will stick to it. This Parser that we are looking at handles only a format thing and it delegates content job! Let’s take a quick look at the ExcelParser implementation. Notice that this dependency ContentFactory creates a new object based on the file name:

public class ContentFactory {
    public Content create(String fileName) {
        if (fileName.contains("book")) {
            return new Book();
        }
        if (fileName.contains("publisher")) {
            return new Publisher();
        }
        throw new RuntimeException("Not supported entity: " + fileName);
    }
}

Ok, let’s get back to the ExcelParser. You can parse Excel file using a library called Apache POI.

But also you should know that the book file contains the following columns: “authorName”, “name”, and “price”. Also, it should be noted that the price is the Integer data type. That means that if you read “price” from the Excel file and you get a String you need to convert that String to an Integer.

The first part of the job(with Apache POI library) is done inside the Parser but the second part where we do some content-specific staff is delegated! Let’s take a look at this part of the implementation:

for(int r = 0; r < rows; r++) {
        Content obj = factory.create(fileName);
        row = sheet.getRow(r);
        if(row != null) {
            for(int c = 0; c < cols; c++) {
                cell = row.getCell((short)c);
                if(cell != null) {
                    cell.setCellType(CellType.STRING);

                    obj.getExcelMetadata().get(c).getSetterFunction().accept(
                                    obj.getExcelMetadata().get(c).getReadingFunction().apply(cell.getStringCellValue())
                            );
                }
            }
            parsedObjects.add(obj);
        }
}

Everything else besides this for loop is just some Apache POI job (like opening the sheet and similar things) specific for Excel. But here you can see that for every row in the Excel, we create a new object: Content obj = factory.create(fileName); using our factory. Then, inside the if(cell != null) we are parsing cell data.

If you look closely inside the loop you cannot conclude which content we are parsing! Do we parse books or publishers? Also, you cannot conclude which columns are included in the book file and which are in the publisher file. That knowledge is delegated to the Content hierarchy!

Inside the Parser implementation, we would like to do two things:

  • Read from the cell somehow.
  • On the obj object, we need to call the appropriate setter to set the value we just read from the cell.

But how do we know if this cell is String or Integer and should we do that conversion before calling setter? How do we know if we are currently reading the authorName or the price field? Well, every Content object has a method getExcelMetadata that gives you metadata about the obj. It gives you readingFunction which should be applied while reading from the excel and setterFunction which should be applied when setting data to the object obj. Now you can better understand this part of the code:

obj.getExcelMetadata().get(c).getSetterFunction().accept(
    obj.getExcelMetadata().get(c).getReadingFunction().apply(cell.getStringCellValue())
);

You retrieve excelMetadata and then apply readingFunction to the cell value(cell.getStringCellValue()). When you have the result you apply the appropriate setter by using setterFunction.

Our Content hierarchy looks like this:

public interface Content {
    <T> Map<Integer, ExcelFieldMetadata<T>> getExcelMetadata();
}

public class Book implements Content {
    private String authorName;
    private String name;
    private double price;

    public String getAuthorName() {
        return authorName;
    }

    public void setAuthorName(String authorName) {
        this.authorName = authorName;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public double getPrice() {
        return price;
    }

    public void setPrice(Double price) {
        this.price = price;
    }

    @Override
    public Map<Integer, ExcelFieldMetadata> getExcelMetadata() {
        Map<Integer, ExcelFieldMetadata> map = new HashMap<>();

        ExcelFieldMetadata<Double> priceFieldMetadata =
                new ExcelFieldMetadata<>(Double::parseDouble, this::setPrice);

        ExcelFieldMetadata<String> authorNameFieldMetadata =
                new ExcelFieldMetadata<>(Function.identity(), this::setAuthorName);

        ExcelFieldMetadata<String> nameFieldMetadata =
                new ExcelFieldMetadata<>(Function.identity(), this::setName);



        map.put(0, authorNameFieldMetadata);
        map.put(1, nameFieldMetadata);
        map.put(2, priceFieldMetadata);
        return map;
    }
}

@Value
public class ExcelFieldMetadata <T> {
    Function<String, T> readingFunction;
    Consumer<T> setterFunction;
}

Implementation of the getExcelMetadata is interesting. On this line new ExcelFieldMetadata<>(Double::parseDouble, this::setPrice); we define that when parsing this particular field we need to apply parseDouble method(which means that you need to convert the String that we get from Excel to the double(because the price is of type double)). Also, we define that we want to call setPrice method to set that field. At the end, we have map.put(2, priceFieldMetadata);. From this line, it can be concluded that this field is third from the beginning. We can also see metadata for other fields as well.

And that’s it, our pattern is almost done! Let’s see a few minor tweaks for the end and how to extend this code.

How to extend this code

If we want to add a new Reader(let’s say Azurewe just need to create a new class AzureReader implements Reader and nothing else. We don’t need to bother with Excel, csvs, books, publishers, etc. The same thing is true if we want to add new content Car. We extend Content and that’s it we don’t bother with AWS, Excel, etc. Adding a new format requires adding a new metadata method into the Content class but that makes sense since now we have new specific knowledge for the content. Let’s say we add CsvParser we need to know particularly for the book in which order fields are, which field is which type, etc.

Last small thing

You can see that the Parser is connected to the Content and those two interact with each other but the Reader is connected neither to the Parser nor to the Content. Another question is: “Does it make sense to do something like this?”:

InputStream inputStream = new AWSReader().read(location);

Well, probably not since you got the InputStream and you still have to convert that InputStream to the book. That means that the Reader always goes with the Parser. A more logical thing is to rewrite Reader to this:

public abstract class Reader {
    private final Parser parser;

    public Reader(Parser parser) {
        this.parser = parser;
    }

    public List<Content> read(String location) {
        return parser.parse(doRead(location), location);
    }

    protected abstract InputStream doRead(String location);
}

public class AWSReader extends Reader {

    public AWSReader(Parser parser) {
        super(parser);
    }

    @Override
    protected InputStream doRead(String location) {
        //Handling auth and reading from aws s3 using aws libraries
    }
}

As you can see now Parser is the dependency of the Reader. A minor change but this is how we connect two hierarchies(and we connect them because they always work together). If you want to use this reader you do that like this:

List<Content> result = new AWSReader(new ExcelParser(new ContentFactory())).read("books.xlsx");

Of course, you need to initialize proper Reader and Parser in your code(and not just hardcode Excel and AWS as I did) but that is not the scope of this tutorial.

Conclusion

And that’s it. You learned bridge pattern in the real world, instructive example!