Design patterns - Observer

From Juneday education
Jump to: navigation, search

Description

In this chapter, we'll discuss the Observer pattern.

Aim of the pattern

The intent of this pattern is to allow for a one-to-many relationship between one object with important state, and many interested objects, subscribing to changes in the important object. It is often described in terms of a "Subject" object which is observable by many "Observers" (or sometimes "Listeners").

Problem description

We often have a situation where something central (the Subject) in our application changes its state, and many components (observers) would be interested to hear about this. The Subject should be loosely coupled with any amount of observers and treat them uniformly. But the actual observers can be totally unrelated objects of very different classes.

class SuperImportantClass {
  List interestedObjects; // what type should we use here?
  // how do we populate the list with interested objects?

  // Times they are a changing...(but Robert stays the same)
  void stateChangesForSomeReason() {
    for (Object o : interestedObjects) {
      //tell them about the changes, but how?
    }
  }
}

As usual, we don't want our classes to know too much about each other, so that things stay on a high abstraction level and things can vary and change without too much impact between classes.

Solution - applying the Observer Pattern

The solution to this problem is to follow the Observer pattern. As usual, there are a few variations of this pattern but we'll explain a common design for it. In order to keep the classes (Subject and Observers) loosely couples (knowing little about each others implementation details), we define two interfaces. We're gonna call the interface for the Subject Observable and the interface for the observers Observer. The Observable interface declares methods every Subject object must have:

  • public void add(Observer o); // an observer wants to register
  • public void delete(Observer o); // unregister observer
  • public void notifyObservers(); // notify all observers

The Observer interface declares (perhaps, but at least in our version) only one method:

  • public void update(Observable o); // this will be called when the Subject wants you to be notified

By using interfaces, we can keep the Subject in happy ignorance about the observers, as well as every observer happily ignorant about the details about the Subject they are observing. We obtain a loose coupling between the actual concrete classes. As long as the Subject adheres to the specification of the Observable interface, and as long as all the observers adhere to the specification of the Observer interface, they can "talk" to each other without knowing too much about the details.

Disclaimer: Note, that in our examples, actually, the observers are kind of coupled with the Observable (they know exactly what class it is made from, and even down-cast the Observable to that type!), so this feature isn't bilateral in our examples. But at least, you can see that the Observable Subject isn't aware of the classes and types of its observers other than that they are adhering to the Observer interface.

Another example

Looking at the Java API is always useful when learning techniques for designing APIs and classes. Often they are quite well designed. The frameworks for graphical interfaces, Swing and AWT use the observer pattern for the event model. Components of the framework provide methods for adding "listeners" (observers) so that objects can get a call-back when an event happens (for instance when someone clicks a button).

This is a version of the observer pattern! A button offers "listeners" (any amount of them) to register themselves with the button, so that the button, when clicked, can notify the listeners that a button click event has occurred.

This means that more than one object can listen to a button and act when the button is clicked. And, actually, the same object can also register with more than one component if the same action should be taken for events from two separate components.

Our example in the video lecture shows that a panic button can have any amount of listeners registered to it. If some one clicks the panic button, all the listeners are notified and run some action. The example simulates (in text, of course, as you have understood by now, we like old school examples), what could happen if someone clicks the panic button of - say for instance - a hotel. The various listeners send of an alarm message in different languages. This allows us to add more languages later on. The code for the actual button need not change - as long as a new listener gets registered, it will get notified when the button is clicked.

The Swing designers have made it simple for us to add more listeners to for instance such a button. They decoupled the action to be taken from the button itself and allowed for multiple actions to be taken. By using an interface ActionListener which simply declares one method actionPerformed(), they make it simple for the button to notify all registered listeners - they all have such a method (or they couldn't register!).

Here's the stupid example Swing code (pay no attention to the uselessness of this application - it is just an example of registering multiple listeners to one single button):

import java.awt.*;
import java.awt.event.*;
import javax.swing.*;

public class ButtonObservers {
  private JButton theButton;
  private JFrame frame;
  
  public ButtonObservers() {
    initComponents();
    layoutComponents();
    frame.setVisible(true);
  }
  private void initComponents() {
    frame = new JFrame("Layout changer");
    frame.setLayout(new BorderLayout());
    frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    frame.setPreferredSize(new Dimension(500,500));
    theButton = new JButton("Hälärm");
    theButton.setFont(new Font("Arial", Font.PLAIN, 40));
    // Add a German panic button listener
    theButton.addActionListener(new ActionListener() {
        @Override
        public void actionPerformed(ActionEvent ae) {
          System.out.println("Hilfe!");
        }
      });
    // Add a Swedish panic button listener
    theButton.addActionListener(new ActionListener() {
        @Override
        public void actionPerformed(ActionEvent ae) {
          System.out.println("Hjälp!");
        }
      });
    // Add an English panic button listener
    theButton.addActionListener(new ActionListener() {
        @Override
        public void actionPerformed(ActionEvent ae) {
          System.out.println("Help!");
        }
      });
    // Add a Spanish panic button listener
    theButton.addActionListener(new ActionListener() {
        @Override
        public void actionPerformed(ActionEvent ae) {
          System.out.println("Socorro!");
        }
      });
  }
  private void layoutComponents() {
    frame.add(theButton, BorderLayout.CENTER);
    frame.pack();
  }
  public static void main(String[] args) {
    new ButtonObservers();
  }
}

Exercises

Task 1 - Add an observable FragList class to the game

Use the version of the game from the Abstract Factory solution (or your own solution if you wrote it yourself).

Add two interfaces to your project:

// Observable.java
public interface Observable {
  public void add(Observer o);
  public void delete(Observer o);
  public void notifyObservers();
}
//Observer.java
public interface Observer {
  public void update(Observable o);
}

Write the FragList.java class and let it implement the Observable interface. It should have the following methods:

  // from the interface Observable:
  public void add(Observer o)
  public void delete(Observer o)
  public void notifyObservers()
  // Methods for the client code (the Game.java simulation):
  public void addKill(Character killer, Character frag)
  public List<String>allFrags()
  public String killsAsString()
  public boolean isOver() // return the boolean isGameOver (you need to have such a variable)
  public void gameOver()  // set the boolean isGameOver to true
  public void incFrags(String name) // increase the number of kills for the player
  public String toString() // return a string with all the player names and their number of kills

The methods gameOver() and incFrags(String name) should call notifyObservers().

If you think this class is too complicated (blame us!), you can copy it from the solution and focus on the logic of the Game.java as described below.

Write three classes which observe the FragList object (they should implement Observer):

public class GameStats implements Observer {

  public GameStats(Observable o) {
    o.add(this);
  }

  public void update(Observable o) {
    if (o instanceof FragList) {
      printStats((FragList)o);
    }
  }

  public void printStats(FragList fl) {
    System.out.println("====Current frag list====");
    System.out.println(fl);
    System.out.println("=========================");
  }
}
public class FinalScore implements Observer{

  public FinalScore(Observable o) {
    o.add(this);
  }

  public void update(Observable o) {
    if (o instanceof FragList) {
      if (((FragList)o).isOver()) {
        printStats((FragList)o);
      }
    }
  }

  public void printStats(FragList fl) {
    System.out.println("====Kills in the game===");
    System.out.println(fl.killsAsString());
    System.out.println("========================");
  }
}
import java.util.Map;
import java.util.TreeMap;
import java.util.List;
public class Characters implements Observer {

  private Map<String,Character> characters;

  public Characters(Observable o) {
    characters = new TreeMap<>();
    o.add(this);
  }

  public void addCharacters(List<Character> list) {
    for (Character c : list) {
      characters.put(c.name(), c);
    }
  }

  public void addCharacter(Character c) {
    characters.put(c.name(), c);
  }

  public Map<String,Character> getCharacters() {
    return characters;
  }

  public void update(Observable o) {
    if (o instanceof FragList) {
      for (String s : ((FragList)o).allFrags()) {
        if (characters.remove(s) != null) {
          ;//System.out.println("Removing " + s);
        }
      }
    }
  }

  @Override
  public String toString() {
    StringBuilder result = new StringBuilder();
    for (Character c : characters.values()) {
      result.append(c.toString());
      result.append("\n");
    }
    return result.toString();
  }
}

Note that when the Characters class is notified about a kill, it doesn't know who just died, so it uses a rather stupid way to figure it out. Or, rather, it doesn't care, it just gets all the kills from the FragList, and removes everyone of them. This is not a clever design (can you figure out something better?). It will try to remove every character who has ever died, every time. We apologize for this stupidity.

In the Game.java application, you should set up the objects like this:

    // Observable list:
    FragList fragList = new FragList();
    //Observers: 
    GameStats gameStats = new GameStats(fragList);
    FinalScore finalScore = new FinalScore(fragList);
    Characters characters = new Characters(fragList);

    // All the players in this simulation:
    CharacterFactory cFactory = new KnightFactory();
    Character player =
      cFactory.createUnarmedCharacter("Sir Playsalot");
    GameEntitiesFactory gef = new NightmareEntitiesFactory();
    List<Character>    opponents = gef.createBadGuys();
    List<WeaponBehavior> weapons = gef.createWeapons();

    // Add the players to the Characters class:
    characters.addCharacters(opponents);
    characters.addCharacter(player);

    // Start the simulation by listing all the characters:
    System.out.println("============ players in this game ===============");
    System.out.println(characters);
    System.out.println("=================================================");

    // The loop with the fights stay as before but add this
    // to the end of the while-loop of the fighting:
        if (c.health() < 1) {
          fragList.incFrags(player.name());
          fragList.addKill(player, c);
        }

    // End the game (end of the main method) like this:
    System.out.println("===========GAME OVER=============");
    fragList.gameOver();
    System.out.println("Final score: ");
    System.out.println(characters);

Challenge - 1 - Improve the design

Now, if you're up to it, let's improve this design a little. This is a little complicated but don't worry. There will be hints!

  • FragList should be converted to a Singleton
  • FragList should now be both observable and an observer
  • The Character abstract base class should become an Observable

public class FragList implements Observable, Observer

It should observe all characters, and all characters should call the update method when they die. The update() method of this new version of FragList now becomes:

  @Override
  public void update(Observable o) {
    System.out.println("Notified by: " + o);
    if (o instanceof Character) {
      if ( ((Character)o).health() < 1) {
        System.out.println(o + " is dead. Adding kill by: " + ((Character)o).slayer());
        addKill( ( (Character)o ).slayer(), (Character)o);
      }
    }
  }

Note that the Game.java client code now doesn't have to call addKill(), the characters will implicitly do this when they die and notify the FragList via the update() method.

The new version of the addKill() in turn, now becomes this:

  public void addKill(Character killer, Character frag) {
    System.out.println(frag.name() + " was killed by " + killer.name());
    List<String> frags = new ArrayList<>();
    if (kills.get(killer.name()) == null) {
      frags.add(frag.name());
      kills.put(killer.name(), frags);
    } else {
      frags = kills.get(killer.name());
      frags.add(frag.name());
      kills.put(killer.name(),frags); 
    }
    incFrags(killer.name());
  }

Note that addKill() calls incFrags() for us. It is incFrags() which notifies all the observers (GameStats, Characters, and, FinalScore):

  public void incFrags(String name) {
    if (fragStats.get(name) == null) {
      fragStats.put(name, new Integer(1));
    } else {
      int frags = fragStats.get(name);
      fragStats.put(name, new Integer(++frags));
    }
    notifyObservers();
  }

You must make the characters notify FragList (remember, FragList is now also an Observer and is observing all the characters). This can actually be done in the abstract Character class, in the takeDamage() method:

  // in the Character class:
  public void takeDamage(int damage, Character adversary) {
    // unsafe for now
    health -= damage;
    if (health < 1) {
      // I died, so notify those interested in this      
      if (slayer == null) {
        slayer = adversary;
        notifyObservers();
      }
    }
  }

Remember that Character has become an Observable by implementing that interface (hence the existance of the notifyObervers() method).

In the constructor of the abstract Character class, the Character registers FragList as an Observer (note that FragList has become a Singleton!):

  public Character(String name) {
    // Register the fraglist as interested in our changes
    add(FragList.fragList());
    this.name = name;
  }

This behavior might be slightly unorthodox, but we don't care. Normally, and observer registers itself with an observable. But we want FragList to observe every character ever created in the game. And FragList doesn't know what characters exist or are being created. So we turn the tables and forcefully register FragList as the observer of every Character being created, by doing this in the abstract superclass Character's constructor as shown above.

We hope you can forgive us for our unorthodox trick.

When FragList is notified about a death, it will want to know who killed who (for its addKill() method call). So we add logic for exposing who was the slayer of a Character, in a new method of Character:

  public Character slayer() {
    return slayer;
  }

You should set the slayer variable when the character dies (see the takeDamage() method above).

What about the Characters class? It stays almost the same. But do you remember the stupid way of removing a dead character? Before, it got all the kills from FragList and tried to remove each of them every time it was notified about a death. Now, we have improved this slightly. The FragList class now has a variable holding the last killed character and a corresponding method for accessing it. The update method of Characters now become:

  public void update(Observable o) {
    if (o instanceof FragList) {
      characters.remove( ((FragList)o).lastKill().name() );
    }
  }

The client code in the Game.java simulation now becomes:

Initializations:

    // Observable list:
    FragList fragList = FragList.fragList();
    //Observers: 
    GameStats gameStats = new GameStats(fragList);
    FinalScore finalScore = new FinalScore(fragList);
    Characters characters = new Characters(fragList);

    CharacterFactory cFactory = new KnightFactory();
    Character player =
      cFactory.createUnarmedCharacter("Sir Playsalot");

    GameEntitiesFactory gef = new NightmareEntitiesFactory();
    List<Character>    opponents = gef.createBadGuys();
    List<WeaponBehavior> weapons = gef.createWeapons();
    
    characters.addCharacters(opponents);
    characters.addCharacter(player);

The fighting logics loop now becomes like it was before, in the original version from the Abstract Factory exercise. This is because now, when a character dies it notifies FragList (which in turn notifies Characters etc). So there is no neeed to call FragList when a character dies - the client code becomes cleaner.

Challenge - 2

This challenge has no suggested solution and is left as a true challenge for those interested. Can you make characters register themselves with the Characters class, when they are constructed?

This would make the client code even cleaner. It would remove the need to send all the characters to the Characters class from the Game.java client code.

In order to achieve this, all the characters must have a reference to the Characters object. An idea could be to convert Characters to a singleton but somehow allow it to be passed a reference to the FragList which it should observe. So the problem you should solve is to

  • Initialize the Characters object with a reference to FragList (as before) so it can observe it and be notified when characters die
  • Let characters add themselves to the Characters class whenever a character is being constructed

It is not recommendable to change the constructor parameters to Character, since it would break a lot of code. So the solution must lie somewhere around creating the Characters as a Singleton but still passing it a reference to FragList the first time the instance is being created. Perhaps some kind of factory method? Use your imagination.

Suggested solutions

Expand using link to the right to see the full content.

You can download source code including suggested solutions TODO: here (github). But here are the source code for the suggested solutions:

Suggested solutions to the first exercise

Interfaces:

public interface Observable {
  public void add(Observer o);
  public void delete(Observer o);
  public void notifyObservers();
}

public interface Observer {
  public void update(Observable o);
}

Observable FragList:

import java.util.Map;
import java.util.TreeMap;
import java.util.List;
import java.util.ArrayList;

public class FragList implements Observable {
  private Map<String, Integer> fragStats;
  private List<Observer> observers;
  private Map<String, List<String>> kills;

  private boolean isOver;

  public FragList() {
    fragStats = new TreeMap<>();
    observers = new ArrayList<>();
    kills  = new TreeMap<>();
  }

  @Override
  public void add(Observer o) {
    if (!observers.contains(o)) {
      observers.add(o);
    }
  }

  @Override
  public void delete(Observer o) {
      observers.remove(o);
  }

  @Override
  public void notifyObservers() {
    for (Observer o : observers) {
      o.update(this);
    }
  }

  public void addKill(Character killer, Character frag) {
    System.out.println(frag.name() + " was killed by " + killer.name());
    List<String> frags = new ArrayList<>();
    if (kills.get(killer.name()) == null) {
      frags.add(frag.name());
      kills.put(killer.name(), frags);
    } else {
      frags = kills.get(killer.name());
      frags.add(frag.name());
      kills.put(killer.name(),frags); 
    }
  }

  public List<String>allFrags() {
    List<String> frags = new ArrayList<String>();
    for(List<String>list : kills.values()){
      frags.addAll(list);
    }
    return frags;
  }

  public String killsAsString() {
    return kills.toString();
  }

  public boolean isOver() { return isOver; }

  public void gameOver() {
    isOver = true;
    notifyObservers();
  }

  public void incFrags(String name) {
    if (fragStats.get(name) == null) {
      fragStats.put(name, new Integer(1));
    } else {
      int frags = fragStats.get(name);
      fragStats.put(name, new Integer(++frags));
    }
    notifyObservers();
  }

  @Override
  public String toString() {
    if (fragStats.isEmpty()) {
      return "No frags yet";
    }
    StringBuilder result = new StringBuilder();
    for (String name : fragStats.keySet()) {
      result.append(name)
        .append(": ")
        .append(fragStats.get(name).toString())
        .append(" frag(s)\n");
    }
    if (result.length() > 0) {
      result.deleteCharAt(result.length()-1);
    }
    return result.toString();
  }
}

Obervers - Characters, GameStats, FinalScore

import java.util.Map;
import java.util.TreeMap;
import java.util.List;

public class Characters implements Observer {

  private Map<String,Character> characters;

  public Characters(Observable o) {
    characters = new TreeMap<>();
    o.add(this);
  }

  public void addCharacters(List<Character> list) {
    for(Character c : list){
      characters.put(c.name(), c);
    }
  }

  public void addCharacter(Character c) {
    characters.put(c.name(), c);
  }

  public Map<String,Character> getCharacters() {
    return characters;
  }

  public void update(Observable o) {
    if (o instanceof FragList) {
      for (String s : ((FragList)o).allFrags()) {
        characters.remove(s);
      }
    }
  }

  @Override
  public String toString() {
    StringBuilder result = new StringBuilder();
    for (Character c : characters.values()) {
      result.append(c.toString());
      result.append("\n");
    }
    return result.toString();
  }
}

public class GameStats implements Observer {
  public GameStats(Observable o) {
    o.add(this);
  }

  public void update(Observable o) {
    if (o instanceof FragList) {
      printStats((FragList)o);
    }
  }

  public void printStats(FragList fl) {
    System.out.println("====Current frag list====");
    System.out.println(fl);
    System.out.println("=========================");
  }
}

public class FinalScore implements Observer {

  public FinalScore(Observable o) {
    o.add(this);
  }

  public void update(Observable o) {
    if (o instanceof FragList) {
      if (((FragList)o).isOver()) {
        printStats((FragList)o);
      }
    }
  }

  public void printStats(FragList fl) {
    System.out.println("====Kills in the game===");
    System.out.println(fl.killsAsString());
    System.out.println("========================");
  }
}

The Game simulation (client code, game engine)

import java.util.List;

public class Game {

  public static void main(String[] args) {
    // Observable list:
    FragList fragList = new FragList();
    //Observers: 
    GameStats gameStats = new GameStats(fragList);
    FinalScore finalScore = new FinalScore(fragList);
    Characters characters = new Characters(fragList);

    CharacterFactory cFactory = new KnightFactory();
    Character player =
      cFactory.createUnarmedCharacter("Sir Playsalot");
    GameEntitiesFactory gef = new NightmareEntitiesFactory();
    List<Character>    opponents = gef.createBadGuys();
    List<WeaponBehavior> weapons = gef.createWeapons();

    characters.addCharacters(opponents);
    characters.addCharacter(player);
    System.out.println("============ players in this game ===============");
    System.out.println(characters);
    System.out.println("=================================================");
    sleep(1000);
    int weaponIndex=0;
    System.out.print(player);
    System.out.println(" enters the woods");
    sleep(1000);
    for (Character c : opponents) {
      System.out.println("<<<Oh no, not another one!");
      System.out.println(player + " meets " + c + " who attacks!");     
      sleep(1000);
      while (c.health() > 0) {        
        c.fight(player);
        sleep(200);
        player.fight(c);
        sleep(200);
        player.fight(c);
        sleep(200);
        player.fight(c);
        sleep(500);
        if (c.health() < 1) {
          fragList.incFrags(player.name());
          fragList.addKill(player, c);
        }
      }
      System.out.println("==============================");
      System.out.println(player + " finds a magic potion and drinks it");
      player.takeDamage(-100);
      if (weaponIndex < weapons.size()) {
        System.out.println(player + " finds " +
                           weapons.get(weaponIndex)
                           + " and upgrades his weapon!");
        player.changeWeapon(weapons.get(weaponIndex));
        weaponIndex++;
      }
    }
    sleep(1000);
    System.out.println("===========GAME OVER=============");
    fragList.gameOver();
    System.out.println("Final score: ");
    System.out.println(characters);
  }

  static void sleep(int millis) {
    try {
      Thread.currentThread().sleep(millis);
    } catch (InterruptedException e) {}
  }
}
====Suggested solutions to the challenge 1====
Abstract base class Character:
<source lang="Java">
import java.util.*;

public abstract class Character implements Observable {

  private WeaponBehavior weapon;
  private String name;
  private int health = 100;
  static private List<Observer> observers = new ArrayList<>();
  private Character slayer;
  
  public void add(Observer o) {
    if (observers.indexOf(o) < 0) {
      observers.add(o);
    }
  }

  public void delete(Observer o) {
    observers.remove(o);
  }

  public void notifyObservers() {
    for (Observer o : observers) {
      o.update(this);
    }
  }
  
  public Character(String name) {
    // Register the fraglist as interested in our changes
    add(FragList.fragList());
    this.name = name;
  }

  public Character(String name, WeaponBehavior weapon) {
    this(name);
    this.weapon = weapon;
  }

  public String name() { return name; }

  public void changeWeapon(WeaponBehavior weapon) {
    this.weapon = weapon;
  }

  public WeaponBehavior weapon() { return weapon; }

  public void takeDamage(int damage) {
    // unsafe for now
    health -= damage;
  }

  // the frag list object must be able to get information
  // about who killed us
  public Character slayer() {
    return slayer;
  }

  public void takeDamage(int damage, Character adversary) {
    // unsafe for now
    health -= damage;
    if (health < 1) {
      // I died, so notify those interested in this      
      if (slayer == null) {
        slayer = adversary;
        notifyObservers();
      }
    }
  }

  public abstract void fight(Character opponent);

  public int health() { return health; }

  public String toString() {
    return name 
      + " with health: " + health 
      + ((weapon != null) ? " carrying " + weapon : " (unarmed)");
  }
}

FragList as both Observable and Observer:

import java.util.Map;
import java.util.TreeMap;
import java.util.List;
import java.util.ArrayList;

public class FragList implements Observable, Observer {

  private Map<String, Integer> fragStats;
  private List<Observer> observers;
  private Map<String, List<String>> kills;
  private boolean isOver;
  private static final FragList instance = new FragList();
  public final static FragList fragList(){ return instance; }

  private Character lastKill;
  
  private FragList() {
    fragStats = new TreeMap<>();
    observers = new ArrayList<>();
    kills  = new TreeMap<>();
  }

  @Override
  public int hashCode() {
    return 17; // this class is a singleton...
  }

  @Override
  public boolean equals(Object o) {
    return o == this;
  }

  @Override
  public void update(Observable o) {
    System.out.println("Notified by: " + o);
    if (o instanceof Character) {
      if ( ((Character)o).health() < 1) {
        System.out.println(o + " is dead. Adding kill by: " + ((Character)o).slayer());
        addKill( ( (Character)o ).slayer(), (Character)o);
      }
    }
  }

  @Override
  public void add(Observer o) {
    if (!observers.contains(o)) {
      observers.add(o);
    }
  }

  @Override
  public void delete(Observer o) {
      observers.remove(o);
  }

  @Override
  public void notifyObservers() {
    for (Observer o : observers) {
      o.update(this);
    }
  }

  public Character lastKill() {
    return lastKill;
  }

  public void addKill(Character killer, Character frag) {
    System.out.println(frag.name() + " was killed by " + killer.name());
    lastKill = frag;
    List<String> frags = new ArrayList<>();
    if (kills.get(killer.name()) == null) {
      frags.add(frag.name());
      kills.put(killer.name(), frags);
    } else {
      frags = kills.get(killer.name());
      frags.add(frag.name());
      kills.put(killer.name(),frags); 
    }
    incFrags(killer.name());
  }

  public List<String>allFrags() {
    List<String> frags = new ArrayList<String>();
    for (List<String>list : kills.values()) {
      frags.addAll(list);
    }
    return frags;
  }

  public String killsAsString() {
    return kills.toString();
  }

  public boolean isOver() { return isOver; }

  public void gameOver() {
    isOver = true;
    notifyObservers();
  }

  public void incFrags(String name) {
    if (fragStats.get(name) == null) {
      fragStats.put(name, new Integer(1));
    } else {
      int frags = fragStats.get(name);
      fragStats.put(name, new Integer(++frags));
    }
    notifyObservers();
  }

  @Override
  public String toString() {
    if (fragStats.isEmpty()) {
      return "No frags yet";
    }
    StringBuilder result = new StringBuilder();
    for (String name : fragStats.keySet()) {
      result.append(name)
        .append(": ")
        .append(fragStats.get(name).toString())
        .append(" frag(s)\n");
    }
    if (result.length() > 0) {
      result.deleteCharAt(result.length() - 1);
    }
    return result.toString();
  }
}

Game.java - The client code for this version:

import java.util.List;

public class Game {

  public static void main(String[] args) {

    // Observable list:
    FragList fragList = FragList.fragList();
    //Observers: 
    GameStats gameStats = new GameStats(fragList);
    FinalScore finalScore = new FinalScore(fragList);
    Characters characters = new Characters(fragList);

    CharacterFactory cFactory = new KnightFactory();
    Character player =
      cFactory.createUnarmedCharacter("Sir Playsalot");

    GameEntitiesFactory gef = new NightmareEntitiesFactory();
    List<Character>    opponents = gef.createBadGuys();
    List<WeaponBehavior> weapons = gef.createWeapons();
    
    characters.addCharacters(opponents);
    characters.addCharacter(player);
    System.out.println("============ players in this game ===============");
    System.out.println(characters);
    System.out.println("=================================================");
    sleep(1000);
    int weaponIndex=0;
    System.out.print(player);
    System.out.println(" enters the woods");
    sleep(1000);
    for (Character c : opponents) {
      System.out.println("<<<Oh no, not another one!");
      System.out.println(player + " meets " + c + " who attacks!");     
      sleep(1000);
      while (c.health() > 0) {
        if(player.health() < 50) {
          player.takeDamage(-50);
        }
        c.fight(player);
        if (c.health() < 0) {
          break;
        }
        sleep(200);
        player.fight(c);
        if (c.health() < 0) {
          break;
        }
        sleep(200);
        player.fight(c);
        if (c.health() < 0) {
          break;
        }
        sleep(200);
        player.fight(c);
        sleep(500);
      }
      System.out.println("==============================");
      if (weaponIndex < weapons.size()) {
        System.out.println(player + " finds " +
                           weapons.get(weaponIndex)
                           + " and upgrades his weapon!");
        player.changeWeapon(weapons.get(weaponIndex));
        weaponIndex++;
      }
    }
    sleep(1000);
    System.out.println("===========GAME OVER=============");
    fragList.gameOver();
    System.out.println("Final score: ");
    System.out.println(characters);
  }

  static void sleep(int millis) {
    try {
      Thread.currentThread().sleep(millis);
    } catch (InterruptedException e) {}
  }
}

Chapter links

Videos

Source code

Books this chapter is a part of

Further reading

Note: the articles above use slightly different designs. And their designs vary slightly compared to our version, but the general idea is the same.

Where to go next

Note, next chapter - next chapter - is a bonus chapter. Slides and videos are missing, but there's a lot of text and examples, as well as source code to look at.

Book TOC | previous chapter | next chapter