© Edward Sciore 2019
Edward ScioreJava Program Designhttps://doi.org/10.1007/978-1-4842-4143-1_10

10. Observers

Edward Sciore1 
(1)
Newton, MA, USA
 

The state of a program changes over time, as new objects get created and existing objects are modified. The program may respond to some of these change events. For example, a large deposit to a foreign-owned bank account might initiate a process that checks for illegal activity. The program may also respond to certain input events such as mouse actions and keyboard entries. For example, a mouse click on a button is normally responded to, but a mouse click on a label is often ignored.

The use of observers is a general-purpose technique for managing a program’s responses to events. An object can maintain a list of observers and notify them when a notable event occurs. This chapter introduces the observer pattern, which is the preferred way to incorporate observers into your code. The chapter gives practical examples of its use and examines various design issues and tradeoffs.

Observers and Observables

Consider the banking demo. Suppose that the bank wants to perform some actions whenever a new account gets created. For example, the marketing department wants to send a “welcome to the bank” information packet to the account owner and the auditing department wants to do background checks on new foreign-owned accounts.

To implement this capability, the Bank class will need a reference to each object that wants to be informed about a new account, so that its newAccount method can notify those objects. The code in Listing 10-1 is a straightforward implementation of this idea, in which the Bank class holds a reference to a MarketingRep and an Auditor object.
public class Bank implements Iterable<BankAccount> {
   private Map<Integer,BankAccount> accounts;
   private int nextacct;
   private MarketingRep rep;
   private Auditor aud;
   public Bank(Map<Integer,BankAccount> accounts, int n,
               MarketingRep r, Auditor a) {
      this.accounts = accounts;
      nextacct = n;
      rep = r; aud = a;
   }
   public int newAccount(int type, boolean isforeign) {
      int acctnum = nextacct++;
      BankAccount ba =
                  AccountFactory.createAccount(type, acctnum);
      ba.setForeign(isforeign);
      accounts.put(acctnum, ba);
      rep.update(acctnum, isforeign);
      aud.update(acctnum, isforeign);
      return acctnum;
   }
   ...
}
Listing 10-1

Adding Observers to the Bank Class

The MarketingRep and Auditor classes are called observer classes , and their objects are called observers. The Bank class is known as the observable class. It notifies its observers when a new account is created. By convention, the notification method is named “update” to denote that the observable is telling its observers that an update has occurred.

The observable-observer relationship is analogous to the relationship between publishers and their subscribers. When a publisher has new material to distribute, it notifies its subscribers. Consequently, the use of observers in a program is also known as the publish-subscribe technique .

The Twitter application is a well-known example of publish-subscribe. A Twitter user has a list of followers. When someone tweets (i.e., publishes) a message, that message is sent to each follower (subscriber) on the list. The publish-subscribe technique is also used by message boards and listservs. If someone sends a message to a listserv, then all subscribers to the listserv receive the message.

The problem with the Bank code in Listing 10-1 is that the bank knows exactly which objects are observing it. In other words, the observable class is tightly coupled to its observer classes. This tight coupling will make it necessary to modify Bank each time the observers change.

For example, suppose that the bank decides to use multiple marketing agents, say one for foreign accounts and another for domestic accounts. The bank will then have two MarketingRep objects observing it. Or suppose that the bank decides to add an observer that logs information about each new account to a file. In this case, Bank would need to hold an additional observer object, this time of type AccountLogger.

The proper way to address this problem is to note that the bank doesn’t really care how many observer objects it has, nor does it care what their classes are. It is sufficient for the bank to simply hold a list of observers. When a new account gets created, it can notify each object in the list.

For this idea to work, the observer classes must implement a common interface. Call this interface BankObserver. It will have a single method, named update, as shown in Listing 10-2.
public interface BankObserver {
   void update(int acctnum, boolean isforeign);
}
Listing 10-2

The BankObserver Interface

The Bank code would then look like Listing 10-3. Note how this design has drastically reduced the coupling between the observable and its observers.
public class Bank implements Iterable<BankAccount> {
   private Map<Integer,BankAccount> accounts;
   private int nextacct;
   private List<BankObserver> observers;
   public Bank(Map<Integer,BankAccount> accounts,
               int n, List<BankObserver> L) {
      this.accounts = accounts;
      nextacct = n;
      observers = L;
   }
   public int newAccount(int type, boolean isforeign) {
      int acctnum = nextacct++;
      BankAccount ba =
                 AccountFactory.createAccount(type, acctnum);
      ba.setForeign(isforeign);
      accounts.put(acctnum, ba);
      observers.forEach(obs->obs.update(acctnum, isforeign));
      return acctnum;
   }
   ...
}
Listing 10-3

An Improved Bank Class

The list provided to the Bank constructor can contain any number of observers, and these observers can belong to any of the classes that implement BankObserver. For a concrete example, consider a simple version of the Auditor class that writes the account number of each new foreign-owned account to the console. Its code might look like Listing 10-4.
public class Auditor implements BankObserver {
   public void update(int acctnum, boolean isforeign) {
      if (isforeign)
         System.out.println("New foreign acct" + acctnum);
   }
}
Listing 10-4

The Auditor Class

The class diagram of Figure 10-1 depicts this relationship between the Bank class and its observers.
../images/470600_1_En_10_Chapter/470600_1_En_10_Fig1_HTML.jpg
Figure 10-1

The Bank class and its observers

The Observer Pattern

This relationship between Bank and its observers is an example of the observer pattern. The basic idea is that an observable object holds a list of observers. When the observable decides to publicize a change to its state, it notifies its observers. This idea is expressed in the class diagram of Figure 10-2.
../images/470600_1_En_10_Chapter/470600_1_En_10_Fig2_HTML.jpg
Figure 10-2

The observer pattern

This class diagram is very similar to the diagram of Figure 10-1. The Bank class is the observable, BankObserver is the observer interface, and Auditor, MarketingRep, and AccountLogger are the observer classes.

Although Figure 10-2 describes the overall architecture of the observer pattern, it is somewhat short on practical details. What should the arguments to the update method be? How does the observable get its list of observers? It turns out that there are several ways to answer these questions, which lead to multiple variations of the observer pattern. The following subsections examine some of the design possibilities.

Push vs. pull

The first issue is to consider the arguments to the update method. In the BankObserver interface of Listing 10-2, update has two arguments, which are the values of the newly created bank account that are of interest to its observers. In a more realistic program, the method might need to have many more arguments. For example, a realistic Auditor class would want to know the owner’s account number, foreign status, and tax id number; whereas the MarketingRep class would want the owner’s account number, name, and address.

This design technique is called push, because the observable “pushes” the values to its observers. The difficulty with the push technique is that the update method must send all values that could possibly be needed by any observer. If the observers require many different values then the update method becomes unwieldy. Moreover, the observable has to guess what values any future observer might need, which can cause the observable to push many unnecessary values “just in case.”

Another design technique, called pull, alleviates these problems. In the pull technique, the update method contains a reference to the observable. Each observer can then use that reference to “pull” the values it wants from the observable.

Listing 10-5 shows the code for BankObserver, revised to use the pull technique. Its update method passes a reference to the Bank object. It also passes the account number of the new account so that the observer can pull the information from the proper account.
public interface BankObserver {
   void update(Bank b, int acctnum);
}
Listing 10-5

Revising the BankObserver Interface to Use Pull

Listing 10-6 shows the revised code for the Auditor observer. Note how its update method pulls the foreign-status flag from the supplied Bank reference.
public class Auditor implements BankObserver {
   public void update(Bank b, int acctnum) {
      boolean isforeign = b.isForeign(acctnum);
      if (isforeign)
         System.out.println("New foreign acct" + acctnum);
   }
}
Listing 10-6

The Revised Auditor Class

The pull technique has a certain elegance, in that the observable provides each observer with tools that enable it to extract the information it needs. One problem with the pull technique is that the observer must go back to the observable to retrieve the desired values, and this time lag can potentially affect correctness.

For example, suppose that a user creates a new domestic account, but soon afterward calls the setForeign method to change it to a foreign owner. If an observer pulls the account information from the bank after setForeign was executed then it will incorrectly assume that the account was created as a foreign account.

Another problem is that the pull technique can only be used when the observable keeps the information the observer wants. For example, suppose that a bank observer wants to be notified each time the deposit method is executed, so it can investigate unusually large deposits. If the bank does not save the amount of each deposit, then pull is not viable. Instead, the bank will need to push the deposit amount via its update method.

Hybrid push-pull designs can be used to balance the tradeoffs between push and pull. For example, the update method could push some values as well as a reference to the observable. Or the update method could push a reference to a relevant object that the observers could then pull from. Listing 10-7 gives an example of this latter interface. In it, the observable pushes a reference to the new BankAccount object, from which the observers can pull the information they need.
public interface BankObserver {
   void update(BankAccount ba);
}
Listing 10-7

A Hybrid Push-Pull BankObserver Interface

Managing the Observer List

The second issue that needs to be examined is how an observable gets its list of observers. In Listing 10-3, the list was passed to the observable via its constructor and remained unchanged throughout the life of the program. However, such a design cannot handle situations in which observers come and go dynamically.

For example, suppose you want an observer to log all bank transactions that occur outside of normal banking hours. One option is for the observer to be continually active. Upon each event notification, the observer checks the current time. If the bank is closed then it logs the event.

The problem is that banking activity is typically heaviest during business hours, which means that the observer will spend a lot of time ignoring most of the notifications it gets. A better idea is to add the observer to the observer list when the bank closes for the evening and remove it when the bank reopens in the morning.

To accommodate this need, observables must provide methods to explicitly add and remove observers from the observer list. These methods are typically called addObserver and removeObserver . With these changes, the Bank code would look like Listing 10-8.
public class Bank implements Iterable<BankAccount> {
   private Map<Integer,BankAccount> accounts;
   private int nextacct;
   private List<BankObserver> observers = new ArrayList<>();
   public Bank(Map<Integer,BankAccount> accounts, int n) {
      this.accounts = accounts;
      nextacct = n;
   }
   public void addObserver(BankObserver obs) {
      observers.add(obs);
   }
   public void removeObserver(BankObserver obs) {
      observers.remove(obs);
   }
   ...
}
Listing 10-8

Another Revision to the Bank Class

This technique of dynamically adding an observer to an observable list is a form of dependency injection. The observable has a dependency on each observer, and this dependency is injected into the observable through its addObserver method . This form of dependency injection is known as method injection (as opposed to the constructor injection of Listing 10-3).

There are two ways to perform method injection. The first way is for another class (such as BankProgram) to add the observers to the list; the other way is for each observer to add itself. The BankProgram code of Listing 10-9 illustrates the first form of method injection.
public class BankProgram {
   public static void main(String[] args) {
      ...
      Bank bank = new Bank(accounts, nextacct);
      BankObserver auditor = new Auditor();
      bank.addObserver(auditor);
      ...
   }
}
Listing 10-9

One Way to perform Method Injection

One advantage to this form of method injection is that the observer object can be expressed as a lambda expression, thereby eliminating the need for an explicit observer class. This idea is shown in Listing 10-10, assuming the BankObserver interface of Listing 10-7.
public class BankProgram {
   public static void main(String[] args) {
      ...
      Bank bank = new Bank(accounts, nextacct);
      bank.addObserver(ba -> {
       if (ba.isForeign())
          System.out.println("New foreign acct: "
                            + ba.getAcctNum());
       });
      ...
   }
}
Listing 10-10

Revising BankProgram to Use a Lambda Expression

Listing 10-11 illustrates the second form of method injection. The Auditor observer receives a reference to the observable Bank object via its constructor, and adds itself to the bank’s observer list.
public class BankProgram {
   public static void main(String[] args) {
      ...
      Bank bank = new Bank(accounts, nextacct);
      BankObserver auditor = new Auditor(bank);
      ...
   }
}
public class Auditor implements BankObserver {
   public Auditor(Bank b) {
      b.addObserver(this);
   }
   ...
}
Listing 10-11

A Second Way to Perform Method Injection

This technique results in a very interesting relationship between an observable and its observers. The observable calls the update method of its observers, yet knows nothing about them. The observers, on the other hand, know which object is calling them. This situation is completely backwards from typical method calls, in which the caller of a method knows who it is calling, and the callee does not know who calls it.

The Generic Observer Pattern in Java

The Java library contains the interface Observer and the class Observable, which are intended to simplify the implementation of the observer pattern. Observer is an all-purpose observer interface, whose code appears in Listing 10-12. Its update method has two arguments, which support hybrid push-pull designs.
interface Observer {
   public void update(Observable obs, Object obj);
}
Listing 10-12

The Observer Interface

The first argument to update is a reference to the observable making the call, for use by the pull technique. The second argument is an object that contains the values sent by the push technique. If the observable wants to push multiple values then it embeds them in a single object. If the observable wants to push no values then it sends null as the second argument.

Observable is an abstract class that implements the list of observers and its associated methods. An observable extends this abstract class in order to inherit this functionality. Its code appears in Listing 10-13.
public abstract class Observable {
   private List<Observer> observers = new ArrayList<>();
   private boolean changed = false;
   public void addObserver(Observer obs) {
      observers.add(obs);
   }
   public void removeObserver(Observer obs) {
      observers.remove(obs);
   }
   public void notifyObservers(Object obj) {
      if (changed)
         for (Observer obs : observers)
            obs.update(this, obj);
      changed = false;
   }
   public void notifyObservers() {
      notifyObservers(null);
   }
   public void setChanged() {
      changed = true;
   }
   ...
}
Listing 10-13

The Observable Class

Note the two different notifyObservers methods. The one-argument version passes the argument to the observer as the second argument to update. The zero-argument version sends null as the second argument to update.

Note also that the notifyObservers methods do nothing until the client first calls setChanged. The purpose of setChanged is to support programs that perform notification periodically, instead of immediately after each change. In such programs, the code doing the periodic notification can call notifyObservers at any time, confident that it will have no effect unless setChanged has been called since the previous notification.

Listing 10-14 shows how to rewrite the banking demo using the Observer and Observable classes and the push technique. The listing contains the relevant code for Bank (the observable) and Auditor (the observer). Note how Bank no longer needs code to manage its observer list and associated methods, because its superclass Observable handles them.
public class Bank extends Observable
                  implements Iterable<BankAccount> {
   ...
   public int newAccount(int type, boolean isforeign) {
      int acctnum = nextacct++;
      BankAccount ba =
                 AccountFactory.createAccount(type, acctnum);
      ba.setForeign(isforeign);
      accounts.put(acctnum, ba);
      setChanged();
      ObserverInfo info =
                   new ObserverInfo(acctnum, isforeign);
      notifyObservers(info);
      return acctnum;
   }
   ...
}
public class Auditor implements Observer {
   public Auditor(Bank bank) {
      bank.addObserver(this);
   }
   public void update(Observable obs, Object obj) {
      ObserverInfo info = (ObserverInfo) obj;
      if (info.isForeign())
         System.out.println("New foreign account: "
                          + info.getAcctNum());
   }
}
Listing 10-14

Rewriting Bank and Auditor Using Observable and Observer

The second argument to the update method is an object of type ObserverInfo. This class embeds the account number and the foreign-status flag in a single object. Its code appears in Listing 10-15.
public class ObserverInfo {
   private int acctnum;
   private boolean isforeign;
   public ObserverInfo(int a, boolean f) {
      acctnum = a;
      isforeign = f;
   }
   public int getAcctNum() {
      return acctnum;
   }
   public boolean isForeign() {
      return isforeign;
   }
}
Listing 10-15

The ObserverInfo Class

Although Observable and Observer implement the basics of the observer pattern, their general-purpose nature has some drawbacks. Observable is an abstract class, not an interface, which implies that the observable is unable to extend any other class. The update method is “one size fits all,” in that an application has to squeeze its pushed data in and out of an object such as ObserverInfo. Because of these drawbacks, and the fact that it’s rather simple to write the code that they provide, it is often better to skip the use of Observable and Observer.

Events

The previous sections have focused on how the Bank class can notify its observers when a new bank account is created. The creation of a new account is an example of an event. In general, an observable may wish to notify its observers about multiple types of event. For example, the version 18 Bank class has four event types. These types correspond to the four methods that affect its bank accounts, namely newAccount, deposit, setForeign, and addInterest. The version 18 bank demo defines these four event types as constants of the enum BankEvent . See Listing 10-16.
public enum BankEvent {
   NEW, DEPOSIT, SETFOREIGN, INTEREST;
}
Listing 10-16

The Version 18 BankEvent Enum

The question is how an observable such as Bank can manage notifications for four different events. There are two issues: how many observer lists the observable should keep, and how many update methods the observer interface should have. It is also possible to create a separate observer interface for each event.

Consider the observer lists. Keeping a single list is simpler, but would mean that every observer would be notified for every event. It is usually better if the observable can keep an observer list for each event, so that its observers can register for only the events that they care about.

Now consider update methods. One option is for the observer interface to have an update method for each event. The advantage is that you can design each method so that its arguments are customized for its event. The disadvantage is that an observer would have to provide an implementation for every method, even if it is interested in just one of them.

The alternative is for the interface to have a single update method. The first argument of the method could identify the event, and the remaining arguments would communicate enough information to satisfy all observers. The downside is that it might be difficult to pack all of that information into a single set of argument values.

For the version 18 bank demo, I chose to use a single update method . Listing 10-17 gives the version 18 BankObserver interface. The update method has three arguments: the event, the affected bank account, and an integer denoting the deposit amount. Not all arguments apply to each event. For example, DEPOSIT observers will use all of the arguments; the NEW and SETFOREIGN observers will use only the event and bank account; and the INTEREST observers will use only the event.
public interface BankObserver {
   void update(BankEvent e, BankAccount ba, int depositamt);
}
Listing 10-17

The Version 18 BankObserver Interface

The version 18 Bank class has an observer list for each of the four event types. For convenience, it bundles those lists into a single map keyed on the event type. Its addObserver method adds an observer to the specified list. The removeObserver method would be similar, but its code was omitted for convenience. Bank also has a notifyObservers method that notifies the observers on the specified list.

Bank has four methods that generate events: newAccount, deposit, setForeign, and addInterest. Version 18 modifies these methods to call the notifyObservers method . Listing 10-18 gives the relevant portion of the code. Note that the third argument to notifyObservers is 0 for all but the deposit method because DEPOSIT is the only event where that value is relevant. The other events ignore that value.
public class Bank implements Iterable<BankAccount> {
   private Map<Integer,BankAccount> accounts;
   private int nextacct;
   private Map<BankEvent,List<BankObserver>> observers
                                       = new HashMap<>();
   public Bank(Map<Integer,BankAccount> accounts, int n) {
      this.accounts = accounts;
      nextacct = n;
      for (BankEvent e : BankEvent.values())
         observers.put(e, new ArrayList<BankObserver>());
   }
   public void addObserver(BankEvent e, BankObserver obs) {
      observers.get(e).add(obs);
   }
   public void notifyObservers(BankEvent e, BankAccount ba,
                               int depositamt) {
      for (BankObserver obs : observers.get(e))
         obs.update(e, ba, depositamt);
   }
   public int newAccount(int type, boolean isforeign) {
      int acctnum = nextacct++;
      BankAccount ba =
                  AccountFactory.createAccount(type, acctnum);
      ba.setForeign(isforeign);
      accounts.put(acctnum, ba);
      notifyObservers(BankEvent.NEW, ba, 0);
      return acctnum;
   }
   public void setForeign(int acctnum, boolean isforeign) {
      BankAccount ba = accounts.get(acctnum);
      ba.setForeign(isforeign);
      notifyObservers(BankEvent.SETFOREIGN, ba, 0);
   }
   public void deposit(int acctnum, int amt) {
      BankAccount ba = accounts.get(acctnum);
      ba.deposit(amt);
      notifyObservers(BankEvent.DEPOSIT, ba, amt);
   }
   public void addInterest() {
      forEach(ba->ba.addInterest());
      notifyObservers(BankEvent.INTEREST, null, 0);
   }
   ...
}
Listing 10-18

The Version 18 Bank Class

The version 18 code for class Auditor appears in Listing 10-19. The class is an observer of two events: NEW and SETFOREIGN. Because it observes two events, it checks the first argument of its update method to determine which event occurred.
public class Auditor implements BankObserver {
   public Auditor(Bank bank) {
      bank.addObserver(BankEvent.NEW, this);
      bank.addObserver(BankEvent.SETFOREIGN, this);
   }
   public void update(BankEvent e, BankAccount ba,
                      depositamt amt) {
      if (ba.isForeign()) {
         if (e == BankEvent.NEW)
            System.out.println("New foreign account: "
                  + ba.getAcctNum());
         else
            System.out.println("Modified foreign account: "
                  + ba.getAcctNum());
      }
   }
}
Listing 10-19

The Version 18 Auditor Class

The version 18 BankProgram code appears in Listing 10-20. The class creates two observers: an instance of Auditor and a lambda expression that observes DEPOSIT events. This observer calls the bank’s makeSuspicious method if it detects a deposit greater than $100,000.
public class BankProgram {
   public static void main(String[] args) {
      SavedBankInfo info = new SavedBankInfo("bank18.info");
      Map<Integer,BankAccount> accounts = info.getAccounts();
      int nextacct = info.nextAcctNum();
      Bank bank = new Bank(accounts, nextacct);
      Auditor aud = new Auditor(bank);
      bank.addObserver(BankEvent.DEPOSIT,
            (event,ba,amt) -> {
               if (amt > 10000000)
                  bank.makeSuspicious(ba.getAcctNum());
            });
      ...
   }
}
Listing 10-20

The Version 18 BankProgram Class

Observers in JavaFX

Events and event observers play an important role in GUI applications. In JavaFX, a user’s interaction with a screen causes a sequence of input events to occur. The JavaFX library specifies several types of input event. Each event type is an object in a class that extends the class Event. Three such classes are MouseEvent, KeyEvent, and ActionEvent. Listing 10-21 shows some common event types for these classes.
MouseEvent.MOUSE_CLICKED
MouseEvent.MOUSE_ENTERED
KeyEvent.KEY_TYPED
ActionEvent.ACTION
Listing 10-21

Four Common JavaFX Event Types

An event type indicates the kind of event that was generated. The target of an event is the node that is responsible for handling it. For example, if the user mouse-clicks at a particular location on the screen, then the topmost node at that location will be the target of a MOUSE_CLICKED event .

Every JavaFX Node object is an observable. A node keeps a separate observer list for each event type. That is, a node will have a list for mouse-click observers, mouse-enter observers, key-typed observers, and so on.

In JavaFX, event observers are called event handlers. Each node has the method addEventHandler, which adds an observer to the node’s observer list for a given event type. This method takes two arguments: the event type of interest, and the reference to the event handler.

An event handler belongs to a class that implements the interface EventHandler. The interface has a single method, named handle. Its code appears in Listing 10-22.
public interface EventHandler {
   void handle(Event e);
}
Listing 10-22

The EventHandler Interface

Listing 10-23 gives the code for the event handler class ColorLabelHandler, whose handle method changes the text of a specified label to have a specified color.
public class ColorLabelHandler
             implements EventHandler<Event> {
   private Label lbl;
   private Color color;
   public ColorLabelHandler(Label lbl, Color color) {
      this.lbl = lbl;
      this.color = color;
   }
   public void handle(Event e) {
      lbl.setTextFill(color);
   }
}
Listing 10-23

The ColorLabelHandler Class

For an example use of event handlers, consider again the AccountCreationWindow program from Listing 9-8 and 9-9. Figure 10-3 displays its initial screen.
../images/470600_1_En_10_Chapter/470600_1_En_10_Fig3_HTML.jpg
Figure 10-3

The initial AccountCreationWindow screen

Listing 10-24 revises the program to have four event handlers:
  • A MOUSE_ENTERED handler on the title label that turns its text red when the mouse enters the region of the label.

  • A MOUSE_EXITED handler on the title label that turns its text back to green when the mouse exits the region of the label. The combination of these two handlers produces a “rollover” effect, where the label temporarily turns red as the mouse rolls over it.

  • A MOUSE_CLICKED handler on the outermost pane that resets the screen by unchecking the check box, setting the value of the choice box to null, and changing the text of the title label back to “Create a new bank account.”

  • A MOUSE_CLICKED handler on the button that uses the values of the check box and choice box to change the text of the title label.

public class AccountCreationWindow extends Application {
   public void start(Stage stage) {
      ...
      Label title = ... // the label across the top
      title.addEventHandler(MouseEvent.MOUSE_ENTERED,
                  new ColorLabelHandler(title, Color.RED));
      title.addEventHandler(MouseEvent.MOUSE_EXITED,
                  e -> title.setTextFill(Color.GREEN));
      Pane p1 = ... // the outermost pane
      p1.addEventHandler(MouseEvent.MOUSE_CLICKED,
           e -> {
             ckbx.setSelected(false);
             chbx.setValue(null);
             title.setText("Create a New Bank Account");
           });
      Button btn = ... // the CREATE ACCT button
      btn.addEventHandler(MouseEvent.MOUSE_CLICKED,
           e -> {
             String foreign = ckbx.isSelected() ?
                              "Foreign " : "";
             String acct = chbx.getValue();
             title.setText(foreign + pref + acct
                                   + " Account Created");
             stage.sizeToScreen();
           });
      ...
   }
}
Listing 10-24

A Revised AccountCreationWindow Class

The first handler uses the ColorLabelHandler class from Listing 10-23. Its handle method will be executed when the mouse enters the region of the title label. The second handler uses a lambda expression to define the handle method. One of the features of a lambda expression (or an inner class) is that it can reference variables (such as title) from its surrounding context. This avoids the need to pass those values into the constructor, as is done in ColorLabelHandler.

The third handler observes mouse clicks on pane p1, and the fourth handler observes mouse clicks on the button. Both of these handlers define their handle method via a lambda expression.

A common way to specify a button handler is to replace the event type MouseEvent.MOUSE_CLICKED with ActionEvent.ACTION. An ACTION event signifies a “submit” request from the user. Buttons support several kinds of submit request, such as a mouse-clicking on the button, touching the button via a touch screen, and pressing the space key when the button has the focus. Using an ACTION event for a button handler is usually better than using a MOUSE_CLICKED event, because a single ACTION event handler will support all these requests.

The Button class also has a method setOnAction, which further simplifies the specification of a button handler. For example, the button handler in Listing 9-9 used setOnAction instead of addEventHandler. The following two statements have the same effect.
   btn.addEventHandler(ActionEvent.ACTION, h);
   btn.setOnAction(h);

JavaFX Properties

The state of a JavaFX node is represented by various properties. For example, two properties of the class ChoiceBox are items, which denotes the list of items the choice box should display, and value, which denotes the currently selected item. For each of a node’s properties, the node has a method that returns a reference to that property. The name of the method is the property name followed by “Property.” For example, ChoiceBox has methods itemsProperty and valueProperty .

Formally, a property is an object that implements the interface Property. Three of its methods are shown in Listing 10-25. Based on these methods, you can rightfully infer that a Property object is both a wrapper and an observable. The methods getValue and setValue get and set the wrapped value, and the method addListener adds a listener to its observer list. These two aspects of a property are examined in the following subsections.
public interface Property<T> {
   T getValue();
   void setValue(T t);
   void addListener(ChangeListener<T> listener);
   ...
}
Listing 10-25

Methods of the Property Interface

Properties as Wrappers

A property’s getValue and setValue methods are rarely used because each node has substitute convenience methods. In particular, if a node has a property named p, then it has convenience methods getP and setP. For an example, Listing 10-26 shows the beginning of the createNodeHierarchy method from Listing 9-9. The calls to the getP and setP methods are in bold.
private Pane createNodeHierarchy() {
   VBox p3 = new VBox(8);
   p3.setAlignment(Pos.CENTER);
   p3.setPadding(new Insets(10));
   p3.setBackground(...);
   Label type = new Label("Select Account Type:");
   ChoiceBox<String> chbx  = new ChoiceBox<>();
   chbx.getItems().addAll("Savings", "Checking",
                          "Interest Checking");
   ...
}
Listing 10-26

The Beginning of the AccountCreationWindow Class

These methods are all convenience methods, and result from the fact that class VBox has properties alignment, padding, and background, and ChoiceBox has the property items. To demonstrate this point, Listing 10-27 gives an alternative version of the code that doesn’t use these convenience methods.
private Pane createNodeHierarchy() {
   VBox p3 = new VBox(8);
   Property<Pos> alignprop = p3.alignmentProperty();
   alignprop.setValue(Pos.CENTER);
   Property<Insets> padprop = p3.paddingProperty();
   padprop.setValue(new Insets(10));
   Property<Background> bgprop = p3.backgroundProperty();
   bgprop.setValue(...);
   Label type = new Label("Select Account Type:");
   ChoiceBox<String> chbx  = new ChoiceBox<>();
   Property<String> itemsprop = chbx.itemsProperty();
   itemsprop.getValue().addAll("Savings", "Checking",
                               "Interest Checking");
   ...
}
Listing 10-27

Revising Listing 10-26 to Use Explicit Property Objects

Properties as Observables

A property is an observable, and maintains a list of observers. When its wrapped object changes state, the property notifies its observers. A property observer is called a change listener , and implements the interface ChangeListener as shown in Listing 10-28.
public interface ChangeListener<T> {
   void changed(Property<T> obs, T oldval, T newval);
}
Listing 10-28

The ChangeListener Interface

The interface consists of one method, named changed. Note that changed is a hybrid push-pull observer method. The second and third arguments push the old and new values to the observer. The first argument is the observable itself, from which the observer can pull additional information. (Technically, this first argument is of type ObservableValue, which is a more general interface than Property. But for simplicity I am ignoring that issue.)

The easiest way to create a change listener is to use a lambda expression. For example, Listing 10-29 gives the code for a listener that you can add to the AccountCreationWindow class . This listener observes the check box ckbx. Executing its code causes the label’s text to turn green if the box becomes selected, and red if the box becomes unselected.
ChangeListener<Boolean> checkboxcolor =
      (obs, oldval, newval) -> {
          Color c = newval ? Color.GREEN : Color.RED;
          ckbx.setTextFill(c);
      };
Listing 10-29

A Check Box Change Listener

To get the change listener to execute, you must add it to the property’s observer list by calling the property’s addListener method, as shown in Listing 10-30. The result is that the check box label will change color from red to green and back again as it is selected and unselected.
ChangeListener<Boolean> checkboxcolor = ... // Listing 10-29
Property<Boolean> p = ckbx.selectedProperty();
p.addListener(checkboxcolor);
Listing 10-30

Attaching a Change Listener to a Property

Listings 10-29 and 10-30 required three statements to create a listener and add it to the observer list of the desired property. I wrote it this way to show you what needs to occur, step by step. In reality, most JavaFX programmers would write the entire code as a single statement, as shown in Listing 10-31.
ckbx.selectedProperty().addListener(
      (obs, oldval, newval) -> {
          Color c = newval ? Color.GREEN : Color.RED;
          ckbx.setTextFill(c);
      });
Listing 10-31

Revising the Check Box Change Listener

Change listeners can also be used to synchronize the behavior of JavaFX controls. Consider again the initial screen of AccountCreationWindow shown in Figure 10-3. Note that the choice box is unselected. If a user clicked the CREATE ACCT button at this point, a runtime error would occur if the code actually tried to create an account.

To eliminate the possibility of error, you can design the screen so that the button is initially disabled and becomes enabled only when an account type is selected. This design calls for adding a change listener to the choice box. Its code is shown in Listing 10-32.
public class AccountCreationWindow extends Application {
   public void start(Stage stage) {
      ...
      chbx.valueProperty().addListener(
            (obj, oldval, newval) ->
                          btn.setDisable(newval==null));
      ...
   }
}
Listing 10-32

Adding Change Listener for the Choice Box

The variable chbx references the choice box. The change listener disables the button if the new value of the choice box becomes null, and enables it otherwise. The result is that the enabled/disabled status of the button is synchronized with the selected/unselected status of the choice box.

Event listeners and change listeners can interact. Recall from Listing 10-24 that the outermost pane p1 of AccountCreationWindow has an event listener that sets the value of the choice box to null when the pane is clicked. This change will cause the change listener of the choice box to fire, which will then disable the button. That is, selecting an item from the choice box enables the button, and clicking on the outer pane disables the button. A user can repeatedly enable and disable the button by selecting an account type from the choice box and then clicking on the outer pane. Try it.

JavaFX Bindings

JavaFX supports the notion of a computed property, which is called a binding. Bindings implement the interface Binding, two of whose methods are shown in Listing 10-33. Note that the primary difference between a binding and a property is that a binding does not have a setValue method. Bindings do not have setValue because their values are computed and cannot be set manually.
public interface Binding<T> {
   public T getValue();
   public void addListener(ChangeListener<T> listener);
   ...
}
Listing 10-33

The Binding Interface

Bindings can be created in several ways, but easiest is to use the methods associated with the type of property you have. For example, properties that wrap objects extend the class ObjectProperty and inherit the method isNull. Listing 10-34 shows how to create a binding for the value property of a choice box.
ChoiceBox chbx = ...
ObjectProperty<String> valprop = chbx.valueProperty();
Binding<Boolean> nullvalbinding = valprop.isNull();
Listing 10-34

An example Binding

The variable nullvalbinding references a Binding object that wraps a boolean. The value of this boolean is computed from the choice box’s value property–in particular, if value wraps a null then the boolean will be true and otherwise false.

When a Binding object is created, it adds itself to the observer list of its property. Consequently, a change to the property value will notify the binding, which can then change its value correspondingly. To help you visualize the situation, look at the diagram of Figure 10-4, which depicts a memory diagram of the three variables of Listing 10-34.
../images/470600_1_En_10_Chapter/470600_1_En_10_Fig4_HTML.jpg
Figure 10-4

The relationship between a binding and its property

The chbk object represents the choice box. It has a reference to each of its properties. The diagram shows only the reference to value and hints at the reference to items. The valprop object represents the value property. It has a reference to its wrapped object (which is the string “savings”) and to its observer list. The diagram shows that the list has at least one observer, which is the binding nullvalbinding. Note that the binding has a similar structure to a property. Its wrapped object is a boolean that has the value false.

When the chbx node changes its wrapped object, say by executing the code valueProperty().setValue(null), the value property will send a change notification to its observers. When the binding receives the notification, it will notice that the new value of the property is null and set the value of its wrapped object to true.

The code of Listing 10-32 created a change listener for the choice box. Listing 10-35 rewrites that code to use a binding. Note how the change listener sets the value of the button’s disable property to be whatever the value of the binding is. There is no need to explicitly check for null as in Listing 10-32, because that check is being performed by the binding.
public class AccountCreationWindow extends Application {
   public void start(Stage stage) {
      ...
      ObjectProperty<String> valprop = chbx.valueProperty();
      Binding<Boolean> nullvalbinding = valprop.isNull();
      nullvalbinding.addListener(
            (obj, oldval, newval) -> btn.setDisable(
                             nullvalbinding.getValue()));
      ...
   }
}
Listing 10-35

Rewriting the Choice Box Change Listener

The code of Listing 10-35 is somewhat difficult to read (and to write!). To simplify things, Property objects have the method bind, which performs the binding for you. Listing 10-36 is equivalent to the code of Listing 10-35.
public class AccountCreationWindow extends Application {
   public void start(Stage stage) {
      ...
      btn.disableProperty()
         .bind(chbx.valueProperty().isNull());
      ...
   }
}
Listing 10-36

Using the Bind Method to Create an Implicit Change Listener

The bind method has one argument, which is a binding (or property). Here, the method’s argument is the binding created by the isNull method. The bind method adds a change listener to that binding so that when its wrapped value changes, the value of the button’s disable property changes to match it. The behavior is exactly the same as in Listing 10-35.

The code of Listing 10-36 is extraordinarily beautiful. The bind method and the isNull method both create change listeners, and these listeners interact via the observer pattern (twice!) to enable the two controls to synchronize their values. And all this occurs behind the scenes, without the client’s knowledge. It is a wonderful example of the usefulness and applicability of the observer pattern.

Summary

An observer is an object whose job is to respond to one of more events. An observable is an object that recognizes when certain events occur, and keeps a list of observers interested in those events. When an event occurs it informs its observers.

The observer pattern specifies this general relationship between observers and observables. But the pattern leaves multiple design issues unaddressed. One issue concerns the update method: what values should an observable push to its observers, and what values should the observers pull from the observable? A second issue concerns how the observable handles multiple types of events: should it treat each event independently, with separate update methods and observer lists, or can it combine event processing somehow? There is no best solution to these issues. A designer must consider the various possibilities for a given situation, and weigh their tradeoffs.

The observer pattern is especially useful for the design of GUI applications. In fact, JavaFX is so infused with the observer pattern that it is practically impossible to design a JavaFX application without making extensive use of observers and observables. And even if an application does not explicitly use observers, the class libraries used by the application almost certainly do.

JavaFX nodes support two kinds of observer: event handlers and change listeners. An event handler responds to input events, such as mouse clicks and key presses. Each event handler belongs to the observer list of some node. A change listener responds to changes in the state of a node. Each change listener belongs to the observer list of some property of a node. By designing event handlers and change listeners appropriately, a JavaFX screen can be given remarkably sophisticated behavior.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.190.156.93