Working With Design Patterns: Bridge

Wednesday Mar 5th 2008 by Jeff Langr
Share:

Separating interface from implementation is a fundamental object-oriented strategy, one that's also the essence of the bridge design pattern. You can use the bridge pattern to help solve the more complex problem of separating tangled hierarchies.

Design patterns exist for some of the most simple, fundamental object-oriented concepts. "These aren't patterns, these are things I've always used just as a regular part of doing OO," you might argue. The bridge pattern falls into this category of a concept so fundamental to OO that it seems to be overkill to consider it a pattern. But, understanding the bridge pattern in its simplest form allows you to understand its value in more complex circumstances.

The Design Patterns book provides a simple summary for the bridge pattern: "Decouple an abstraction from its implementation so that the two can vary independently." To construct code using the bridge pattern in Java, you define an interface and classes that implement the interface. That's it! The interface provides a consistent abstraction with which clients can interact. The interface also isolates the client from any knowledge of the specific type of implementation and from any implementation details.

The collections class framework in the Java API provides several examples of use of the bridge pattern. Both the ArrayList and LinkedList concrete classes implement the List interface. The List interface provides common, abstract concepts, such as the abilities to add to a list and to ask for its size. The implementation details vary between ArrayList and LinkedList, mostly with respect to when memory is allocated for elements in the list.

Your application can take advantage of this common interface: Wherever you would have a reference variable of either ArrayList or LinkedList type, replace the variable's type with the List interface type:

public void find(List<Patron> patrons, Patron requested) {
   for (Patron patron: patrons)
      if (patron.equals(requested))
         return patron;
   return null;
}

The logic in the find method doesn't care if the list of patrons is implemented as an array or a linked list. This use of the bridge pattern improves the maintainability of your code. If all of your code is structured this way, you can change your code from using an ArrayList to a LinkedList, or vice versa, in one place.

If you do test-driven development (TDD), the bridge pattern is indispensible when it comes down to implementing mocks. If your test targets a class whose collaborator is giving you testing headaches, the bridge pattern can be of assistance. You extract an interface from the troublesome collaborator. You then can build a test double (aka a mock) for the collaborator. This test double is a class that implements the same interface and thus emulates, or mocks, the collaborator. Your unit test can inject the test double into the target, thus "fixing" the collaborator for purposes of testing. The test target is none the wiser!

Listing 1 shows two classes that represent another potential application of the bridge pattern. Each of BookHtmlFormatter and BookPrintFormatter provides a print method. The job of this method is to return a nicely formatted string for a book in a library system, with the formatting dependent upon whether the book information is going to be printed on a receipt or as part of a web page.

Listing 1: Simple implementations.

// BookHtmlFormatter.java
public class BookHtmlFormatter {
   public String print(Book book) {
      StringBuilder builder = new StringBuilder();
      builder.append(book.getClassification() + "<br />");
      builder.append("<table>");
      builder.append("<tr><th>Author</th><th>Title</th>
                          <th>Year</th></tr>");
      builder.append("<tr>");
      builder.append("<td>" + book.getAuthor() + "</td>");
      builder.append("<td>" + book.getTitle()  + "</td>");
      builder.append("<td>" + book.getYear()   + "</td>");
      builder.append("</tr>");
      builder.append("</table>");
      return builder.toString();
   }
}

// BookPrintFormatter.java
import static util.StringUtil.*;

public class BookPrintFormatter {
   public String print(Book book) {
      StringBuilder builder = new StringBuilder();
      builder.append(book.getClassification() + EOL);
      builder.append("Author: " + book.getAuthor() + EOL);
      builder.append("Title:  " + book.getTitle()  + EOL);
      builder.append(book.getYear() + EOL);
      return builder.toString();
   }

}

You can extract a common interface, and have each of the two formatter classes implement this interface (see Listing 2).

Listing 2: A simple bridge.

public interface Formatter {
   String print(Book book);
}

public class BookPrintFormatter implements Formatter {
   // ...
}

public class BookHtmlFormatter implements Formatter {
   // ...
}

The predominance of client code now can work with a Formatter reference. If you design your application carefully, you'll need only instantiate the concrete formatter types in one place.

Formatter formatter = new BookHtmlFormatter();
String text = formatter.print(book);

Simplifying Class Combinations

Now, suppose you also need to print movie information in a slightly different format on both the web and on receipts. In fact, you probably also want to print similar information for other media types in the library, such as audio tapes. Further, you may need to start printing onto different media in a different format, such as microfiche. These new requirements could require you to create an explosion of combinations: BookFicheFormatter, TapeHtmlFormatter, TapeFicheFormatter, and so on. Instead, the strength of the bridge pattern will help you keep some sanity in class organization.

The path to deriving an improved solution through use of the bridge pattern is recognizing two things. First, the code in BookHtmlFormatter and BookPrintFormatter violates the single responsibility principle. The print method combines formatting information with the data (and organization of such) to be formatted. Second, any new combined derivative (for example, TapeHtmlFormatter) will necessarily need to introduce redundancies with the existing implementation, something you really want to avoid.

By using the bridge pattern, you separate the two SRP-violating concerns into two separate hierarchies. A Printer hierarchy represents the client's abstract need to extract information and organize a printout. The need to format elements properly, depending on the output medium (HTML or printer), is abstracted into a separate Formatter hierarchy. See Figure 1 for the UML.

Listing 3 shows the code for the abstractions, derived by a number of simple refactorings against the current implementation. Note the introduction of a new class, Detail. The Detail class is a simple structure that holds onto a label and the corresponding value for that label.

Listing 3: Abstractions for the bridge.

public abstract class Printer {
   public String print(Formatter formatter) {
      return formatter.format(getHeader(), getDetails());
   }

   abstract protected Detail[] getDetails();
   abstract protected String getHeader();
}

public interface Formatter {
   String format(String header, Detail[] details);
}

public class Detail {
   private String label;
   private String value;

   public Detail(String label, String value) {
      this.label = label;
      this.value = value;
   }

   public String getLabel() {
      return label;
   }

   public String getValue() {
      return value;
   }

}

Th BookPrinter class (see Listing 4) is a derivative of the Printer abstract class. It fills in the holes in the template method print, defined in Printer, by supplying code for the getHeader and getDetails abstract methods. These methods take specific Book information and organize them for the report. The Printer method print simply delegates this captured information over to the Formatter object. Listings 5 and 6 provide the code for the two formatter implementations.

Listing 4: BookPrinter.

public class BookPrinter extends Printer {
   private Book book;

   public BookPrinter(Book book) {
      this.book = book;
   }

   protected String getHeader() {
      return book.getClassification();
   }

   protected Detail[] getDetails() {
      return new Detail[] {
         new Detail("Author", book.getAuthor()),
         new Detail("Title",  book.getTitle()),
         new Detail("Year",   book.getYear()) };
   }
}

The formatter classes hide all of the ugliness and detail behind making a report look nice, whether HTML tags or appropriate space padding is required.

Listing 5: The HTML formatter class.

public class HtmlFormatter implements Formatter {
   @Override
   public String format(String header, Detail[] details) {
      StringBuilder builder = new StringBuilder();
      builder.append(header + "<br />");

      builder.append("<table>");
      appendDetailHeader(builder, details);
      appendDetail(builder, details);
      builder.append("</table>");

      return builder.toString();
   }

   private void appendDetail(StringBuilder builder,
      Detail[] details) {
      builder.append("<tr>");
      for (int i = 0; i < details.length; i++)
         builder.append("<td>" + details[i].getValue() +
                        "</td>");
      builder.append("</tr>");
   }

   private void appendDetailHeader(StringBuilder builder,
      Detail[] details) {
      builder.append("<tr>");
      for (int i = 0; i < details.length; i++)
         builder.append("<th>" + details[i].getLabel() +
                        "</th>");
      builder.append("</tr>");
   }
}

Listing 6: The plain print formatter class.

import static util.StringUtil.*;

public class PrintFormatter implements Formatter {

   @Override
   public String format(String header, Detail[] details) {
      StringBuilder builder = new StringBuilder();
      builder.append(header + EOL);
      int longest = getLongestSize(details);
      for (int i = 0; i < details.length; i++) {
         int length = details[i].getLabel().length();
         String pad = spaces(longest - length + 1);
         builder.append(details[i].getLabel() + ":" + pad +
            details[i].getValue() + EOL);
      }
      return builder.toString();
   }

   private String spaces(int number) {
      StringBuilder builder = new StringBuilder();
      for (int i = 0; i < number; i++)
         builder.append(' ');
      return builder.toString();
   }

   private int getLongestSize(Detail[] details) {
      int longest = 0;
      for (int i = 0; i < details.length; i++)
         if (details[i].getLabel().length() > longest)
            longest = details[i].getLabel().length();
      return longest;
   }
}

The client code changes to take advantage of the bridge pattern:

Formatter formatter = new HtmlFormatter();
String result = new BookPrinter(book).print(formatter);

Short-term, implementing the bridge pattern can be overkill. Sometimes, the separation isn't worth it: A couple of classes in a couple of potentially related hierarchies might be more simply stated as four combined subclasses. But, imagine the work behind half a dozen media types and another half a dozen print formats. Look for frequency of change and common sense to be your guide.



Click here for a larger image.

Figure 1: Bridge

About the Author

Jeff Langr is a veteran software developer with over a quarter century of professional software development experience. He's written two books, including Agile Java: Crafting Code With Test-Driven Development (Prentice Hall) in 2005. Jeff is contributing a chapter to Uncle Bob Martin's upcoming book, Clean Code. Jeff has written over 75 articles on software development, with over thirty appearing at Developer.com. You can find out more about Jeff at his site, http://langrsoft.com, or you can contact him via email at jeff at langrsoft dot com.

Share:
Home
Mobile Site | Full Site
Copyright 2017 © QuinStreet Inc. All Rights Reserved