André Restivo

Design Patterns

1. Command Pattern

In this exercise, we will implement the Command pattern.

  testImplementation 'org.mockito:mockito-core:3.7.7'
@Test
public void stringDrink() {
    StringDrink drink = new StringDrink("ABCD");
    assertEquals("ABCD", drink.getText());
    drink.setText("DCBA");
    assertEquals("DCBA", drink.getText());
}
void execute(StringDrink drink);
@Test
public void stringInverter() {
    StringDrink drink = new StringDrink("ABCD");
    StringInverter si = new StringInverter();
    si.execute(drink);
    assertEquals("DCBA", drink.getText());
}
Tip: Strings 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 StringBuilder. You can also use StringBuilder's reverse() method.
@Test
public void stringCaseChanger() {
    StringDrink drink = new StringDrink("aBcD");
    StringCaseChanger cc = new StringCaseChanger();
    cc.execute(drink);
    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('A', 'X');
    sr.execute(drink);
    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();
    StringCaseChanger cc = new StringCaseChanger();
    StringReplacer sr = new StringReplacer('A', 'X');

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

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

    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();
  StringCaseChanger cc = new StringCaseChanger();
  StringReplacer sr = new StringReplacer('A', 'X');

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

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

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

  si.undo(drink);
  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 transformerGroup() {
  StringDrink drink = new StringDrink( "AbCd-aBcD");

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

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

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

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

    StringInverter si = new StringInverter();
    StringCaseChanger cc = new StringCaseChanger();
    StringReplacer sr = new StringReplacer('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(drink);

    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 can order drinks by specifying their recipes. However, our clients want to be notified whenever their favorite bars go into happy hour. We can use the observer pattern for this.

  public boolean isHappyHour() {};
  public void startHappyHour() {}; 
  public void endHappyHour() {};
@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);
public void addObserver(BarObserver observer) {
  observers.add(observer);
}

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

public void notifyObservers() {
  for (BarObserver observer : observers)
    if (isHappyHour()) observer.happyHourStarted(this);
    else observer.happyHourEnded(this);
}
void wants(StringDrink drink, 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)).happyHourEnded(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() {
    StringInverter si = new StringInverter();
    StringCaseChanger cc = new StringCaseChanger();
    StringReplacer sr = new StringReplacer('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();

    stringBar.order(drink, recipe);
    assertEquals("dCbX-DcBa", drink.getText());
}
void wants(StringDrink drink, 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();

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

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

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

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

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

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

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

    client.wants(drink, 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 more straightforward. Only two different alien races are known to frequent StringBars: the Ferengi and the Romulans.

Contrary to humans, 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(StringDrink drink, StringRecipe recipe, StringBar bar) {
      strategy.wants(drink, recipe, bar);
  }

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

    FerengiClient client = new FerengiClient();

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

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

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

    client.wants(drink, 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();

    RomulanClient client = new RomulanClient();

    // Recipe is ordered immediately
    client.wants(drink, 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: