Design Patterns

Exercises

Design Patterns

1. Command Pattern

In this exercise we will implement the Command pattern.

testCompile group: 'org.mockito', name: 'mockito-core', version: '2.25.1'
@Test
public void stringDrink() {
    StringDrink drink = new StringDrink("ABCD");
    assertEquals("ABCD", drink.getText());
    drink.setText("DCBA");
    assertEquals("DCBA", drink.getText());
}
void execute();
@Test
public void stringInverter() {
  StringDrink drink = new StringDrink("ABCD");
  StringInverter si = new StringInverter(drink);
  si.execute();
  assertEquals("DCBA", drink.getText());
}
Tip: String are immutable. Concatenating strings in order to construct a larger string is inefficient as a lot of strings have to be constructed. The smart way to implement this is to use a StringBuffer. You can also use StringBuffer's reverse() method.
@Test
public void stringCaseChanger() {
  StringDrink drink = new StringDrink("aBcD");
  StringCaseChanger cc = new StringCaseChanger(drink);
  cc.execute();
  assertEquals("AbCd", drink.getText());
}
Tip: Use the methods Character.isLowerCase(char), Character.toUpperCase(char) and Character.toLowerCase(char).
@Test
public void stringReplacer() {
  StringDrink drink = new StringDrink("ABCDABCD");
  StringReplacer sr = new StringReplacer(drink, 'A', 'X');
  sr.execute();
  assertEquals("XBCDXBCD", drink.getText());
}
Tip: Use the method String.replace(char, char).
@Test
public void stringRecipe() {
  StringDrink drink = new StringDrink( "AbCd-aBcD");

  StringInverter si = new StringInverter(drink);
  StringCaseChanger cc = new StringCaseChanger(drink);
  StringReplacer sr = new StringReplacer(drink, 'A', 'X');

  List<StringTransformer> transformers = new ArrayList<>();
  transformers.add(si);
  transformers.add(cc);
  transformers.add(sr);

  StringRecipe recipe = new StringRecipe(transformers);
  recipe.mix();

  assertEquals("dCbX-DcBa", drink.getText());
}

You have now implemented the Command pattern where the test is the Client, the StringRecipe is the Invoker, the StringTransfomer is the command, the three concrete transformers are the ConcreteCommands and the StringDrink is the receiver:

Notice some benefits of this design:
@Test
public void transformUndo() {
  StringDrink drink = new StringDrink( "AbCd-aBcD");

  StringInverter si = new StringInverter(drink);
  StringCaseChanger cc = new StringCaseChanger(drink);
  StringReplacer sr = new StringReplacer(drink, 'A', 'X');

  si.execute();
  cc.execute();
  sr.execute();

  sr.undo();
  assertEquals("dCbA-DcBa", drink.getText());

  cc.undo();
  assertEquals("DcBa-dCbA", drink.getText());

  si.undo();
  assertEquals("AbCd-aBcD", drink.getText());
}

2. Composite Pattern

At this point it’s easy to combine StringTransformers (the steps in our recipes) to assemble different StringRecipes. However, we expect that there will be some particular sequences of steps that appear in many different recipes. How can we reuse these recurring sequences of steps? The Composite pattern will help.

@Test
public void tranformerGroup() {
  StringDrink drink = new StringDrink( "AbCd-aBcD");

  StringInverter si = new StringInverter(drink);
  StringCaseChanger cc = new StringCaseChanger(drink);

  List<StringTransformer> transformers = new ArrayList<>();
  transformers.add(si);
  transformers.add(cc);

  StringTransformerGroup tg = new StringTransformerGroup(transformers);
  tg.execute();

  assertEquals("dCbA-DcBa", drink.getText());
}
@Test
public void tranformerComposite() {
  StringDrink drink = new StringDrink("AbCd-aBcD");

  StringInverter si = new StringInverter(drink);
  StringCaseChanger cc = new StringCaseChanger(drink);
  StringReplacer sr = new StringReplacer(drink, 'A', 'X');

  List<StringTransformer> transformers1 = new ArrayList<>();
  transformers1.add(si);
  transformers1.add(cc);
  StringTransformerGroup tg1 = new StringTransformerGroup(transformers1);

  List<StringTransformer> transformers2 = new ArrayList<>();
  transformers2.add(sr);
  transformers2.add(cc);
  StringTransformerGroup tg2 = new StringTransformerGroup(transformers2);

  List<StringTransformer> transformers3 = new ArrayList<>();
  transformers3.add(tg1);
  transformers3.add(tg2);

  StringRecipe recipe = new StringRecipe(transformers3);
  recipe.mix();

  assertEquals("DcBx-dCbA", drink.getText());
}

You have now implemented the Composite pattern where the StringTransformer is the Component, and the TransformerGroup is the Composite:

Notice some benefits of this design:

3. Observer Pattern

We will now implement a bar where clients will be able to order drinks by specifying their recipes. However, our clients want to be notified every time their favorite bars go into happy hour. We can use the observer pattern for this.

@Test
public void happyHour() {
  Bar bar = new StringBar();
  assertFalse(bar.isHappyHour());

  bar.startHappyHour();
  assertTrue(bar.isHappyHour());

  bar.endHappyHour();
  assertFalse(bar.isHappyHour());
}
void happyHourStarted(Bar bar);
void happyHourEnded(Bar bar);
void addObserver(BarObserver observer) {
  observers.add(observer);
}

void removeObserver(BarObserver observer) {
  observers.remove(observer);
}

void notifyObservers() {
  for (BarObserver observer : observers)
    if (isHappyHour()) observer.happyHourStarted(this);
    else observer.happyHourEnded(this);
}
void wants(StringRecipe recipe, StringBar bar);
@Test
public void addObserver() {
  Bar bar = new StringBar();

  HumanClient clientMock = Mockito.mock(HumanClient.class);
  bar.addObserver(clientMock);

  Mockito.verify(clientMock, Mockito.never()).happyHourStarted(bar);
  Mockito.verify(clientMock, Mockito.never()).happyHourEnded(bar);

  bar.startHappyHour();
  Mockito.verify(clientMock, Mockito.times(1)).happyHourStarted(bar);
  Mockito.verify(clientMock, Mockito.never()).happyHourEnded(bar);

  bar.endHappyHour();
  Mockito.verify(clientMock, Mockito.times(1)).happyHourStarted(bar);
  Mockito.verify(clientMock, Mockito.times(1)).happyHourStarted(bar);
}

@Test
public void removeObserver() {
  Bar bar = new StringBar();

  HumanClient clientMock = Mockito.mock(HumanClient.class);
  bar.addObserver(clientMock);
  bar.removeObserver(clientMock);

  bar.startHappyHour();
  bar.endHappyHour();

  Mockito.verify(clientMock, Mockito.never()).happyHourStarted(bar);
  Mockito.verify(clientMock, Mockito.never()).happyHourEnded(bar);
}

You have now implemented the Observer pattern where the BarObserver is the Observer, the HumanClient is the ConcreteObserver, the Bar is the Subject, and the StringBar is the ConcreteSubject:

Notice the following benefit of this design:

4. Strategy Pattern

Our clients may want to adopt different approaches to their drink ordering. We can use the strategy pattern for this!

private StringRecipe getRecipe(StringDrink drink) {
  StringInverter si = new StringInverter(drink);
  StringCaseChanger cc = new StringCaseChanger(drink);
  StringReplacer sr = new StringReplacer(drink, 'A', 'X');

  List<StringTransformer> transformers = new ArrayList<>();
  transformers.add(si);
  transformers.add(cc);
  transformers.add(sr);

  StringRecipe recipe = new StringRecipe(transformers);
  return recipe;
}

@Test
public void orderStringRecipe() {
  StringBar stringBar = new StringBar();
  StringDrink drink = new StringDrink("AbCd-aBcD");
  StringRecipe recipe = getRecipe(drink);

  stringBar.order(recipe);
  assertEquals("dCbX-DcBa", drink.getText());
}
void wants(StringRecipe recipe, StringBar bar);
void happyHourStarted(StringBar bar);
void happyHourEnded(StringBar bar);
@Test
public void impatientStrategy() {
  StringBar stringBar = new StringBar();
  StringDrink drink = new StringDrink("AbCd-aBcD");
  StringRecipe recipe = getRecipe(drink);

  ImpatientStrategy strategy = new ImpatientStrategy();
  HumanClient client = new HumanClient(strategy);

  // Recipe is ordered immediately
  client.wants(recipe, stringBar);
  assertEquals("dCbX-DcBa", drink.getText());
}

@Test
public void smartStrategyStartOpened() {
  StringBar stringBar = new StringBar();
  StringDrink drink = new StringDrink("AbCd-aBcD");
  StringRecipe recipe = getRecipe(drink);

  SmartStrategy strategy = new SmartStrategy();
  HumanClient client = new HumanClient(strategy);

  // Recipe is ordered immediately as happy hour was already under way
  stringBar.startHappyHour();
  client.wants(recipe, stringBar);
  assertEquals("dCbX-DcBa", drink.getText());
}

@Test
public void smartStrategyStartClosed() {
  StringBar stringBar = new StringBar();
  StringDrink drink = new StringDrink("AbCd-aBcD");
  StringRecipe recipe = getRecipe(drink);

  SmartStrategy strategy = new SmartStrategy();
  HumanClient client = new HumanClient(strategy);
  stringBar.addObserver(client); // this is important!

  client.wants(recipe, stringBar);
  assertEquals("AbCd-aBcD", drink.getText());

  // Recipe is only ordered here
  stringBar.startHappyHour();
  assertEquals("dCbX-DcBa", drink.getText());
}

You have now implemented the Strategy pattern where the OrderingStrategy is the Strategy, the ImpatientStrategy and SmartStrategy are the ConcreteStrategies, and the Context is the HumanClient:

Notice some benefits of this design:

5. Factory-Method Pattern

Humans are complicated! Fortunately, aliens are much simpler. There are only two different alien races known to frequent StringBars: the Ferengi and the Romulans.

Contrary to humans, that are configured with a different OrderingStrategy when they are born, all Ferengi use the SmartStrategy, while all Romulans use the ImpatientStrategy.

public abstract class AlienClient implements Client {
  private OrderingStrategy strategy;

  public AlienClient() {
      this.strategy = createOrderingStrategy();
  }

  @Override
  public void happyHourStarted(Bar bar) {
      strategy.happyHourStarted((StringBar) bar);
  }

  @Override
  public void happyHourEnded(Bar bar) {
      strategy.happyHourEnded((StringBar) bar);
  }

  @Override
  public void wants(StringRecipe recipe, StringBar bar) {
      strategy.wants(recipe, bar);
  }

  protected abstract  OrderingStrategy createOrderingStrategy();
}
@Test
public void ferengiAlreadyOpened() {
  StringBar stringBar = new StringBar();
  StringDrink drink = new StringDrink("AbCd-aBcD");
  StringRecipe recipe = getRecipe(drink);

  FerengiClient client = new FerengiClient();

  // Recipe is ordered immediately
  stringBar.startHappyHour();
  client.wants(recipe, stringBar);
  assertEquals("dCbX-DcBa", drink.getText());
}

@Test
public void ferengiStartClosed() {
  StringBar stringBar = new StringBar();
  StringDrink drink = new StringDrink("AbCd-aBcD");
  StringRecipe recipe = getRecipe(drink);

  FerengiClient client = new FerengiClient();
  stringBar.addObserver(client); // this is important!

  client.wants(recipe, stringBar);
  assertEquals("AbCd-aBcD", drink.getText());

  // Recipe is only ordered here
  stringBar.startHappyHour();
  assertEquals("dCbX-DcBa", drink.getText());
}

@Test
public void romulan() {
  StringBar stringBar = new StringBar();
  StringDrink drink = new StringDrink("AbCd-aBcD");
  StringRecipe recipe = getRecipe(drink);

  RomulanClient client = new RomulanClient();

  // Recipe is ordered immediately
  client.wants(recipe, stringBar);
  assertEquals("dCbX-DcBa", drink.getText());
}

You have now implemented the Factory-Method pattern where the AlienClient is the Creator, the two different alien races are the ConcreteCreators, the OrderingStrategy is the Product and the two different strategies are the ConcreteProducts:

Notice some benefits of this design: