The Observer pattern (also known as Listener, or Publish-Subscribe) is particularly useful for graphical applications.
The general idea is that one or more objects (the subscribers) register their interest in being notified of changes to another object (the publisher). The subscribers can also change the state of the publisher, although this is not obligatory (one might call these "active" observers, as opposed to "passive" ones, which do not change the state of the publisher). Interfaces are used to ensure that publishers and subscribers have only minimal knowledge of each other.
When using Swing, there is a choice among using Observable, Observer , or the many XXXListener interfaces which apply to GUI components.
Although Observer is indeed an interface, Observable is neither an interface nor an abstract base class : it is a concrete class. This is very unusual. Almost all implementations of Design Patterns define the important items as either interfaces or abstract base classes, not as concrete classes.
Extended Example
CurrentPortfolio is a non-graphical class which encapsulates
data regarding a set of stocks - a stock portfolio. It is the central abstraction
of the user interface, and a number of other classes need to interact with
it, both as active and passive listeners. It extends Observable
since it is a non-graphical class.
package hirondelle.stocks.portfolio; import java.util.Observable; import java.util.*; import hirondelle.stocks.util.Args; import hirondelle.stocks.quotes.Stock; /** * The central abstraction of this package, representing the current selection of * stocks of interest to the end user * (a {@link hirondelle.stocks.portfolio.Portfolio}). * * <P><tt>CurrentPortfolio</tt> may be used as an example implementation for any * application which edits items one at a time. * * <p>The {@link #isUntitled} and {@link #getNeedsSave} properties are particularly * significant. They influence the file menu actions. For example, a * <tt>CurrentPortfolio</tt> for which {@link #isUntitled} is true cannot be deleted. * * <p><tt>CurrentPortfolio</tt> is an {@link java.util.Observable}. To minimize spurious * updates, related {@link java.util.Observer} objects need to call * {@link java.util.Observable#notifyObservers()} explicitly. This is important in this * application, since quotes are fetched from the web each time the current portfolio is * updated, and this is a relatively expensive operation. */ public final class CurrentPortfolio extends Observable { /** * Constructor. * * @param aPortfolio is the set of stocks of current interest to the user; no * defensive copy is made of <tt>aPortfolio</tt>. * @param aNeedsSave is true only if this <tt>CurrentPortfolio</tt> * has been edited by the end user, and these edits have not yet been saved. */ public CurrentPortfolio(Portfolio aPortfolio, NeedsSave aNeedsSave) { Args.checkForNull(aPortfolio); fPortfolio = aPortfolio; fNeedsSave = aNeedsSave.getValue(); // upon construction of the main window, an update is desired in order to // synch the gui with the current portfolio. This update is called explicitly. // Thus, setChanged needs to be set here, since it's default value is false. setChanged(); } /** * Enumeration for the two states of <tt>aNeedsSave</tt> passed to the constructor. * Use of an enumeration forces the caller to create a constructor call which has * high clarity. */ public enum NeedsSave { TRUE(true), FALSE(false); boolean getValue() { return fToggle; } private final boolean fToggle; private NeedsSave(boolean aToggle) { fToggle = aToggle; } } /** * Revert to an untitled <tt>Portfolio</tt> which does not need a save. */ public void clear() { setPortfolio(Portfolio.getUntitledPortfolio()); setNeedsSave(false); } /** * Return <tt>true</tt> only if the current <tt>Portfolio</tt> has never been * saved under a user-specified name, neither in this session, nor in any other. Such a * <tt>Portfolio</tt> appears as untitled in the display. */ public boolean isUntitled() { return fPortfolio.isUntitled(); } /** * Return the {@link Portfolio} of current interest to the user. */ public Portfolio getPortfolio() { return fPortfolio; } /** * Change the {@link Portfolio} of current interest to the user. */ public void setPortfolio(Portfolio aPortfolio) { Args.checkForNull(aPortfolio); fPortfolio = aPortfolio; setChanged(); } /** * Return the name of this <tt>CurrentPortfolio</tt>. */ public String getName() { return fPortfolio.getName(); } /** * Change the name of this <tt>CurrentPortfolio</tt>. * @param aName has the same conditions as * {@link hirondelle.stocks.portfolio.Portfolio#setName(String)}. */ public void setName(String aName) { fPortfolio.setName(aName); setChanged(); } /** * Return the {@link hirondelle.stocks.quotes.Stock} objects in this * <tt>CurrentPortfolio</tt>. */ public Set<Stock> getStocks() { return fPortfolio.getStocks(); } /** * Change the stocks in this <tt>CurrentPortfolio</tt>. * @param aStocks has the same conditions as * {@link hirondelle.stocks.portfolio.Portfolio#setStocks(Set)} */ public void setStocks(Set<Stock> aStocks) { fPortfolio.setStocks(aStocks); setChanged(); } /** * Return <tt>true</tt> only if this <tt>CurrentPortfolio</tt> has unsaved * edits. */ public boolean getNeedsSave() { return fNeedsSave; } /** * Indicate that this <tt>CurrentPortfolio</tt> either does or does not have any * unsaved edits. */ public void setNeedsSave(boolean aNeedsSave) { fNeedsSave = aNeedsSave; setChanged(); } // PRIVATE // private Portfolio fPortfolio; private boolean fNeedsSave; }
FileSaveAction is an "active" Observer : it both reacts to the CurrentPortfolio and acts upon it. It reacts to CurrentPortfolio since it is enabled only when the CurrentPortfolio has unsaved edits. When FileSaveAction is executed, on the other hand, it acts upon CurrentPortfolio by changing its state to "no unsaved edits".
(Note that FileSaveAction.actionPerformed can simply ignore
its ActionEvent argument, since it already has a reference to
the CurrentPortfolio.)
package hirondelle.stocks.file; import java.awt.event.*; import javax.swing.*; import java.util.*; import hirondelle.stocks.portfolio.PortfolioDAO; import hirondelle.stocks.portfolio.CurrentPortfolio; import hirondelle.stocks.util.ui.UiUtil; import java.util.logging.Logger; import hirondelle.stocks.util.Util; /** * Save the edits performed on the {@link CurrentPortfolio}, and update the display * to show that the <tt>CurrentPortfolio</tt> no longer needs a save. */ public final class FileSaveAction extends AbstractAction implements Observer { /** * Constructor. * * @param aCurrentPortfolio is to be saved by this action. */ public FileSaveAction(CurrentPortfolio aCurrentPortfolio) { super("Save", UiUtil.getImageIcon("/toolbarButtonGraphics/general/Save")); fCurrentPortfolio = aCurrentPortfolio; fCurrentPortfolio.addObserver( this ); putValue(SHORT_DESCRIPTION, "Save edits to the current portfolio"); putValue( ACCELERATOR_KEY, KeyStroke.getKeyStroke(KeyEvent.VK_S, ActionEvent.CTRL_MASK) ); putValue(LONG_DESCRIPTION, "Save edits to the current portfolio"); putValue(MNEMONIC_KEY, new Integer(KeyEvent.VK_S) ); } public void actionPerformed(ActionEvent e) { fLogger.info("Saving edits to the current portfolio."); PortfolioDAO portfolioDAO = new PortfolioDAO(); portfolioDAO.save( fCurrentPortfolio.getPortfolio() ); fCurrentPortfolio.setNeedsSave(false); fCurrentPortfolio.notifyObservers(); } /** * Synchronize the state of this object with the state of the * <tt>CurrentPortfolio</tt> passed to the constructor. * * This action is enabled only when the <tt>CurrentPortfolio</tt> is titled and * needs a save. */ public void update(Observable aPublisher, Object aData) { setEnabled( fCurrentPortfolio.getNeedsSave() && !fCurrentPortfolio.isUntitled() ); } // PRIVATE // private CurrentPortfolio fCurrentPortfolio; private static final Logger fLogger = Util.getLogger(FileSaveAction.class); }
SummaryView is graphical component, which is a passive listener of another graphical component called QuoteFilterFactory (see Filter table rows for more information). The role of the QuoteFilterFactory is to return an object which allows its listeners - such as SummaryView - to limit calculations to only those stocks which satisfy some particular criterion. When the user selects a GUI element in QuoteFilterFactory, then a PropertyChangeEvent is broadcast to its listeners.
package hirondelle.stocks.quotes; import java.util.*; import java.util.logging.*; import javax.swing.*; import java.awt.*; import java.beans.*; import java.math.BigDecimal; import hirondelle.stocks.table.QuoteFilter; import hirondelle.stocks.util.Args; import hirondelle.stocks.util.ui.UiConsts; import hirondelle.stocks.util.ui.UiUtil; import hirondelle.stocks.table.QuoteFilterFactory; import hirondelle.stocks.portfolio.CurrentPortfolio; import hirondelle.stocks.util.Util; /** * Component placed on the main screen to present summary information * regarding the {@link CurrentPortfolio} to the user. * * <P> Monetary values are presented with zero decimal places, and percentages * are presented with 2 decimal places. */ public final class SummaryView extends JPanel implements PropertyChangeListener { /** * Constructor. * * @param aCurrentPortfolio summarized by this class * @param aQuoteFilterFactory source of a {@link QuoteFilter} used to * limit this summary to only certain items. */ public SummaryView( CurrentPortfolio aCurrentPortfolio, QuoteFilterFactory aQuoteFilterFactory ) { Args.checkForNull(aCurrentPortfolio); fCurrentPortfolio = aCurrentPortfolio; fQuoteFilterFactory = aQuoteFilterFactory; fQuoteFilterFactory.addPropertyChangeListener(this); LayoutManager layout = new BoxLayout(this, BoxLayout.Y_AXIS); setLayout(layout); add(getSummaryFields()); add(Box.createVerticalStrut(UiConsts.ONE_SPACE)); add(getTimeLastUpdateField()); add(Box.createVerticalGlue()); add(getStatusField()); } /** * Update this <tt>SummaryView</tt> in response to selection by user of a new * criterion for filtering items. * * <P> Listens to the {@link QuoteFilterFactory} passed to the constructor. * @param event processed only if its <tt>getPropertyName</tt> is equal to * <tt>QuoteFilterFactory.SELECTED_FILTER</tt>. */ public void propertyChange(PropertyChangeEvent event) { boolean isUndesiredEvent = !event.getPropertyName().equals( QuoteFilterFactory.SELECTED_FILTER ); if (isUndesiredEvent) { fLogger.finer("SummaryView DISCARDING event..."); } else { fLogger.finer("SummaryView processing event..."); updateView(); } } /** * Display summary information for the {@link CurrentPortfolio}, adding any filtering * according to the {@link QuoteFilterFactory} passed to the constructor. * * <P>Use a {@link ColorTip} to draw the user's attention to the fresh * quote information, by highlighting the time of last update for a few seconds. * @param aQuotes contains a {@link Quote} for every * {@link Stock} in the {@link CurrentPortfolio}. */ void setQuotes(Collection<Quote> aQuotes) { fQuotes = aQuotes; ColorTip colorTip = new ColorTip(0, 2, fTimeLastUpdate, Color.yellow); colorTip.start(); updateView(); } /** * Present short text to the user indicating success or failure of the most recent * {@link FetchQuotesAction}. * * <P>In the case of failure, <tt>aMessage</tt> should indicate remedial measures. * @param aMessage has visible content */ void showStatusMessage(String aMessage) { Args.checkForContent(aMessage); fStatusMessage.setText(aMessage); } // PRIVATE // private CurrentPortfolio fCurrentPortfolio; private QuoteFilterFactory fQuoteFilterFactory; private Collection<Quote> fQuotes; private JLabel fBookValue; private JLabel fCurrentValue; private JLabel fProfit; private JLabel fPercentageProfit; private JLabel fTimeLastUpdate; private JLabel fStatusMessage; private static final Logger fLogger = Util.getLogger(SummaryView.class); private JComponent getSummaryFields() { JPanel content = new JPanel(); LayoutManager layout = new BoxLayout(content, BoxLayout.X_AXIS); content.setLayout(layout); content.add(getValueFields()); content.add(Box.createHorizontalGlue()); content.add(getProfitFields()); return content; } private JComponent getValueFields() { JPanel content = new JPanel(); content.setLayout(new GridBagLayout()); fBookValue = UiUtil.addSimpleDisplayField( content, "Book Value", null, UiUtil.getConstraints(0,0), false ); fBookValue.setToolTipText("Acquisition cost of the portfolio"); fCurrentValue = UiUtil.addSimpleDisplayField( content, "Current Value", null, UiUtil.getConstraints(1, 0), false ); fCurrentValue.setToolTipText("Current value of the portfolio"); return content; } private JComponent getProfitFields() { JPanel content = new JPanel(); content.setLayout(new GridBagLayout()); fProfit = UiUtil.addSimpleDisplayField( content, "Profit", null, UiUtil.getConstraints(0, 0), false ); fProfit.setToolTipText("Current value minus book value"); fPercentageProfit = UiUtil.addSimpleDisplayField( content, "% Profit", null, UiUtil.getConstraints(1, 0), false ); fPercentageProfit.setToolTipText("Profit divided by book value, as percent"); return content; } private JComponent getTimeLastUpdateField() { JPanel content = new JPanel(); content.setLayout(new GridBagLayout()); fTimeLastUpdate = UiUtil.addSimpleDisplayField( content, "Last Update", null, UiUtil.getConstraints(0, 0), false ); return content; } private JComponent getStatusField() { JPanel content = new JPanel(); content.setLayout(new BoxLayout(content, BoxLayout.X_AXIS)); fStatusMessage = UiUtil.addSimpleDisplayField( content, "Status", null, UiUtil.getConstraints(0,0), false ); content.add(Box.createHorizontalGlue()); return content; } private void updateView() { QuoteFilter filter = fQuoteFilterFactory.getSelectedFilter(); Collection<Quote> filteredQuotes = filter.sift(fQuotes); fBookValue.setText(getBookValue(filteredQuotes)); fCurrentValue.setText(getCurrentValue(filteredQuotes)); fProfit.setText(getProfit(filteredQuotes)); fPercentageProfit.setText(getPercentageProfit(filteredQuotes)); fTimeLastUpdate.setText(UiUtil.getLocalizedTime(new Date())); } private String getBookValue(Collection<Quote> aQuotes) { Number value = fCurrentPortfolio.getPortfolio().getBookValue(aQuotes); return UiUtil.getLocalizedInteger(value); } private String getCurrentValue(Collection<Quote> aQuotes) { Number value = fCurrentPortfolio.getPortfolio().getCurrentValue(aQuotes); return UiUtil.getLocalizedInteger(value); } private String getProfit(Collection<Quote> aQuotes) { Number value = fCurrentPortfolio.getPortfolio().getProfit(aQuotes); return UiUtil.getLocalizedInteger(value); } private String getPercentageProfit(Collection<Quote> aQuotes) { BigDecimal value = fCurrentPortfolio.getPortfolio().getPercentageProfit(aQuotes); return UiUtil.getLocalizedPercent(value); } }
|
|