http://www.developer.com/

Back to article

Working With Design Patterns: Mediator


June 5, 2008

Unless you've been living in Antarctica, you've encountered the self-checkout line at the grocery store. And, if you're reading this article, well, you're a geek like me, so you have no fear of self-checkout. You might imagine one of the simplest possible use cases for this process:

Purchase a Single Item Via Credit Card

  1. Customer scans one item
  2. Touch screen displays item barcode, description, price
  3. Customer selects "pay now"
  4. Touch screen offers selection of cash or credit card
  5. Customer selects credit card
  6. Touch screen says "complete transaction on pinpad device"
  7. Customer swipes card on pin pad
  8. Pin pad requests pin
  9. Customer enters pin
  10. System verifies credit card #, exp date
  11. System prints receipt
  12. Touch screen prints "please take your purchase"

No doubt there are dozens more use cases, involving accepting and dispensing cash and change, weighing items, looking up items, entering an item code for unscannable items, scanning a store loyalty card, user neglects to put item in bagging area, and so on. The simple use case and the few additional possible use cases I listed should suggest that there are a good number of devices and/or subsystems involved:

  • Scanner
  • Touch screen
  • Pin pad
  • Printer
  • Inventory
  • Produce scale
  • Change dispenser
  • Bagging area scale
  • Bill accepter
  • Bill dispenser

The first two lines alone of the use case require three subsystems: scanner, touch screen, and inventory. You might consider that many of these devices could send messages to each other to accomplish actions. For example, the touch screen system could send a message to the pin pad device when the customer selects "credit card payment." After the customer swipes a credit card, the pin pad device could send a message back to the touch screen system. There might be a similar exchange protocol between the bagging scale and the touch screen system.

Even if having devices talk to one another directly was the best design, however, it's not even feasible in this environment. The self-checkout lane is actually an amalgam of devices produced by different manufacturers. They don't know how to talk to each other because there's no standard protocol for these devices.

Enter the mediator, an object whose job is to unify by acting as a conduit for messages that need to go from device to device. The touch screen might be a candidate for mediator, but technically it's just another dumb device that the grocery chain purchases from a vendor. The self-checkout line system itself will need to appoint its own mediator.

To demonstrate the mediator pattern, I'll consider only those first two lines of the use case. Interfaces for the subsystems involved appear in Listing 1.

Listing 1: Subsystem interfaces.

// Scanner.java
public interface Scanner {
   void addScannerListener(ScannerListener listener);
}

// ScannerListener.java
public interface ScannerListener {
   public void scanned(String barcode);
}

// TouchScreenDisplay.java
public interface TouchScreenDisplay {
   void appendPurchasedItem(String text);
}

// Inventory.java
import java.math.*;

public interface Inventory {
   BigDecimal price(String barcode);
   String description(String barcode);
}

The Scanner interface represents a really simple system. The scanner implementation waits until a barcode passes across its beams; then, it sends the barcode off to a registered listener (ScannerListener), and goes back to waiting for another barcode.

The TouchScreenDisplay interface is one-half of the touch screen device, the half that allows information to appear on the monitor that the customer interacts with. For simplicity purposes, the interface as presented here constrains the display to only being able to show purchase records. (To fully support the self-checkout process, the interface would need many more methods, including the ability to support displaying a menu, a confirmation message, an error message, and so on. Another interface would be required to capture customer interactions with the touch screen: payNow(), selectPaymentMethod(), etc.)

The Inventory subsystem returns the price and description for a given barcode by looking it up in the store's database.

Here is the core of a test that verifies the first two use case lines:

   new SelfChecker(scanner, display, inventory);
   scannerListener.scanned(EGGS_BARCODE);
   assertEquals(lastDisplayed, "12345 - Green Eggs - $2.95");

Paraphrased, this test says: create a SelfChecker object, passing in references to the scanner, display, and inventory. Then simulate scanning an item with a specific barcode (eggs). Finally, verify that the last thing displayed on the touch screen shows the details for eggs. The SelfChecker object will mediate all the required interactions among scanner, display, and inventory to accomplish this goal.

Fleshing out the details that complete the test requires the use of test doubles (see Listing 2). A stub Scanner implementation simply holds onto the ScannerListener object passed to addScannerListener. A TouchScreenDisplay "spy" object captures the last String argument sent over to the appendPurchasedItem method. An Inventory mock object verifies the appropriate arguments, then returns a hard-coded value.

Listing 2: SelfCheckerTest.

import static org.junit.Assert.*;
import java.math.*;
import org.junit.*;

public class SelfCheckerTest {
   private ScannerListener scannerListener;
   private String lastDisplayed;
   private static final String EGGS_BARCODE = "12345";

   @Test
   public void singleItemSale() {
      Scanner scanner = new Scanner() {
         @Override
         public void addScannerListener(ScannerListener listener) {
            SelfCheckerTest.this.scannerListener = listener;
         }
      };

      TouchScreenDisplay display = new TouchScreenDisplay() {
         @Override
         public void appendPurchasedItem(String text) {
            SelfCheckerTest.this.lastDisplayed = text;
         }
      };

      Inventory inventory = new Inventory() {
         @Override
         public String description(String barcode) {
            assertTrue(barcode.equals(EGGS_BARCODE));
            return "Green Eggs";
         }

         @Override
         public BigDecimal price(String barcode) {
            assertTrue(barcode.equals(EGGS_BARCODE));
            return new BigDecimal("2.95");
         }
      };

      new SelfChecker(scanner, display, inventory);
      scannerListener.scanned(EGGS_BARCODE);
      assertEquals(lastDisplayed, "12345 - Green Eggs - $2.95");
   }
}

In the middle of scanner, display, and inventory subsystems lies the SelfChecker object—the mediator (see Listing 3). The SelfChecker sends a message to the scanner to add itself as a ScannerListener. Upon receiving a scanned message, the SelfChecker obtains description and price information for a barcode by sending a message to the Inventory object, and finally sends a message to the touch screen display, passing the appropriate output string.

Listing 3: SelfChecker.

public class SelfChecker implements ScannerListener {
   private final TouchScreenDisplay display;
   private final Inventory inventory;

   public SelfChecker(Scanner scanner, TouchScreenDisplay display,
                      Inventory inventory) {
      this.display = display;
      this.inventory = inventory;
      scanner.addScannerListener(this);
   }

   @Override
   public void scanned(String barcode) {
      String purchase =
         String.format("%s - %s - $%s",
               barcode,
               inventory.description(barcode),
               inventory.price(barcode));
      display.appendPurchasedItem(purchase);
   }
}

In lieu of the mediator pattern, a different solution might involve creating classes that adapt each of the devices in the self-checker system. Imagine the entanglement among objects of these adapter types as they sent messages to each other. Most adapter classes would have dependencies on most other classes, promoting all the negatives of a highly-coupled system. Ultimately, all the messages flying about would make the code considerably more confusing and costly to maintain.

Figure 1: Mediator.

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 has contributed a couple chapters 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.

Sitemap | Contact Us

Thanks for your registration, follow us on our social networks to keep up-to-date