COMP 310
Fall 2017

Lec10:  Faking Out the GUI with Factories 

Home  Info  Canvas   Java Resources  Eclipse Resources  Piazza

Closures

First, let's discuss the general notion of closures in Object-Oriented Programming and Design (OOP/D).   Also, a quick review of lambda functions in Java.

The 4 Pillars of Abstraction:

  1. Abstract Structure
  2. Abstract Behavior
  3. Abstract Construction
  4. Abstract Environment

Factory Design Pattern

What is a "factory"?    A factory, in colloquial usage, is simply an entity that makes things.  Does one really know what goes on inside of a  factory? (Do you really want to know?)   The bottom line is that a factory is an encapsulated entity that hides away the construction process of a product -- you ask the factory to make you something and it makes it for you.    You don't have to concern yourself with the details of how to build the product.   For all you know, the product isn't even a new one -- it could simply be a recycled object that's being handed to you.    

Want something different?   Get a different factory.   Same abstract construction process (i.e. "make me one!") different factory -->  different product, different product behavior.   Notice that in order for a factory to be useful, at the abstract level, it must return an abstract product, typed either as an abstract class or interface.   Otherwise, changing factories would have no effect because if they were defined as returning a product of a specific concrete class, different factories would all return exactly the same thing.

The key to using factories is to realize that your code is interested simply in obtaining an instance of an object and not about how to make it.

Technically, there are two kinds of "factories" in OO design:

 

Faking Out the GUI with Factories or "The power of abstracted construction"

If the GUI is decoupled from the actual mechanics of Ballworld, how is it able to manage the arbitrary combining of strategies and the subsequent construction of a ball from an arbitrarily combined strategy?

The problem is that the "Add to lists" button doesn't initiate the construction of an strategy object, it only give the system the ability to use a strategy, either for combining or ball instantiation. 

A common "traditional" solution is to save a string object that is the strategy's name and then parse the string to figure out if it is a compound strategy and if so, what its components are.   The problem here is that the act of clicking the "Add to lists" button clearly defines the need to include that specific strategy in the system and the act of clicking the "Combine!" button clearly defines the creation of a binary pair of strategies, but the knowledge of both of these clear acts is lost when that information is encoded as a string that needs to be subsequently parsed.

In an OO system, you always want to capture all the information when it happens and not have to reconstruct it later because part of the information was subsequently lost.

How do we accomplish the full capture of information in this situation?

The trick is the properly abstract what exactly the buttons are doing.    Note that netiher the Add to lists nor Combine! buttons care about the specifics of what strategy(s) they are adding or combining.   What they are really doing is to specify the desire to construct the strategy at some later time.   That is, what the two buttons are doing is to add the ability to construct the specified strategy or the ability to construct the specified strategy pair.

But yet, the system, and especially the GUI, still does not care about the specifics of that construction.   Hence, factories.

Factory passing

What the Add to lists and Combine! buttons do is to add a factory to the system that can construct the desired strategy or strategy pair:

  /**
   * An interface that defines a factory that instantiates 
   * a specific IUpdateStrategy
   */
  public interface IStrategyFac { 
    /**
     * Instantiate the specific IUpdateStrategy for which this factory is defined.
     * @return An IUpdateStrategy instance.
     */
    public IUpdateStrategy make();
  }

On the Model side:

The following are utility methods supplied by the BallModel class:

  
    /**
     * Returns an IStrategyFac that can instantiate the strategy specified by
     * classname. Returns a factory for a beeping error strategy if classname is null. 
     * The toString() of the returned factory is the classname.
     * 
     * @param classname  Shortened name of desired strategy
     * @return A factory to make that strategy
     */
    public IStrategyFac makeStrategyFac(final String classname) {
        if (null == classname) return _errorStrategyFac;
        return new IStrategyFac() {
            /**
             * Instantiate a strategy corresponding to the given class name.
             * @return An IUpdateStrategy instance
             */
            public IUpdateStrategy make() {
                return loadStrategy(fixName(classname));
            }

            /**
             * Return the given class name string
             */
            public String toString() {
                return classname;
            }
        };
    }

    /**
     * Returns an IStrategyFac that can instantiate a MultiStrategy with the two
     * strategies made by the two given IStrategyFac objects. Returns null if
     * either supplied factory is null. The toString() of the returned factory
     * is the toString()'s of the two given factories, concatenated with "-". 
     * If either factory is null, then a factory for a beeping error strategy is returned.
     * 
     * @param stratFac1 An IStrategyFac for a strategy
     * @param stratFac2 An IStrategyFac for a strategy
     * @return An IStrategyFac for the composition of the two strategies
     */
    public IStrategyFac combineStrategyFacs(final IStrategyFac stratFac1, final IStrategyFac stratFac2) {
        if (null == stratFac1 || null == stratFac2) return _errorStrategyFac;
        return new IStrategyFac() {
            /**
             * Instantiate a new MultiStrategy with the strategies from the given strategy factories
             * @return A MultiStrategy instance
             */
            public IUpdateStrategy make() {
                return new MultiStrategy(stratFac1.make(), stratFac2.make());
            }

            /**
             * Return a string that is the toString()'s of the given strategy factories concatenated with a "-"
             */
            public String toString() {
                return stratFac1.toString() + "-" + stratFac2.toString();
            }
        };
    }

Both of the above methods return instances of IStrategyFac, one uses the class name of a strategy and the other uses two other IStrategyFac objects:

makeStrategyFac:  This method takes an abbreviated class name of a strategy and creates a factory that is capable of instantiating an instance of that strategy.   The loadStrategy method  of BallModel is used to dynamically load a strategy instance given its fully qualified class name.   Another utility method. fixName, is used to save the user's typing by converting the abbreviated strategy class name into it's fully qualfied form.  The factory object's toString method is overridden so that it returns the abbreviated strategy class name.   This is used by the drop lists as its display to the user.

combineStrategyFacs: This method takes two IStrategyFac objects and creates an IStrategyFac that is capable of making a MultiStrategy instance.   The two strategies held by the MultiStrategy instance will be the strategies made by the supplied IStrategyFacs.   To create a meaningful display for the user, the toString method is overridden to return a concatenation of the toStrings from the supplied factories. 

Notice how both methods return objects whose behaviors are at some later time, are dependent on the input parameters to the method that made them.   That is, the input parameters to makeStrategyFac and combineStrategyFacs beome integral parts of the IStrategyFac objects that are returned.    The magic of closures enables the returned factory objects to access those input parameters and use them long after those methods have terminated.    In technical terms, we say that these methods are currying their input parameters into their return values.   This is a bread-and-butter functional programming technique that forms the backbone of how compound behaviors are created in theoretical computer science (see for instance, this discussion by Prof. Cartwright from our old Comp211 course).    We see here that this important concept has some very useful manifestations in object-oriented programming.

 

On the View side:

The drop-lists on the GUI are JComboBox<TDropListItem>'s, which most people simply load with strings to display.   However, JComboBox actually takes TDropListItem's, not Strings.   The drop-list is created by the displaying the return value of each object's toString() method.    All the Add to lists and Combine! buttons do is call utility methods supplied by BallModel to create IStrategyFacs and add the returned factory, treated as as generic TDropListItem's to both drop-lists.   All the Make Selected Ball button does is to use the selected factory to instantiate the strategy needed to load a new Ball.

     public class BallGUI<TDropListItem> extends JFrame {

		// other code elided
		
        /**
         * Adapter back to the model for control tasks.
         */
         private IModelControlAdapter<TDropListItem> _modelControlAdpt;             
         
         /**
          * The top drop list, used to select what strategy to use in a new ball and
          * to switch the switcher to.
          */
         private JComboBox<TDropListItem> _list1DL;

         /**
          * Bottom drop list, used for combining with the top list selection.
          */
         private JComboBox<TDropListItem> _list2DL; 

         
         public BallGUI(IModelControlAdapter<TDropListItem> modelCtrlAdpt, IModelUpdateAdapter modelUpdateAdpt) {
             this._modelControlAdpt = modelCtrlAdpt;  
             // etc.
         }
         
         private void initGUI() {
             // other code elided
             
             _addBtn.addActionListener(new ActionListener() {
                @Override
                public void actionPerformed(ActionEvent e) {
                    TDropListItem o = _modelControlAdpt.addStrategy(_inputTF.getText());
                    if (null == o) return; // just in case
				
                    _list1DL.insertItemAt(o, 0);
                    _list2DL.insertItemAt(o, 0);
                }
             });

             _makeSelectedBtn.addActionListener(new ActionListener() {
                @Override
                public void actionPerformed(ActionEvent e) {
                    _modelControlAdpt.makeBall(_list1DL.getItemAt(_list1DL.getSelectedIndex()));
                }
             });
             
             // "Combine" button code left as exercise for the student
         }
         
         // etc.
     }

WARNING!  It is tempting to use the getSelectedItem() method of a JComboBox<TDropListItem>, but unfortunately, for legacy reasons,  _list1DL.getSelectedItem() is defined as returning an object as type Object, not type TDropListItem.   Thus, above the following type of call is used, e.g.  _list1DL.getItemAt(_list1DL.getSelectedIndex()),  because the getItemAt method returns an object of type TDropListItem.   

The IModelControlAdapter is thus defined as such:

/**
 * Adapter that the view uses to communicate to the model for non-repetitive control tasks such as manipulating strategies.
 * @param TDropListItem  The type of objects put into the view's drop lists.
 * @author swong
 *
 */
public interface IModelControlAdapter<TDropListItem> {

    /**
     * Take the given short strategy name and return a corresponding something to put onto both drop lists.
     * @param classname  The shortened class name of the desired strategy
     * @return Something to put onto both the drop lists.
     */
    public TDropListItem addStrategy(String classname);

    /**
     * Make a ball with the selected short strategy name.
     * @param selectedItem  A shorten class name for the desired strategy
     */
    public void makeBall(TDropListItem selectedItem);

    /**
     * Return a new object to put on both lists, given two items from the lists.
     * @param selectedItem1  An object from one drop list
     * @param selectedItem2 An object from the other drop list
     * @return An object to put back on both lists.
     */
    public TDropListItem combineStrategies(TDropListItem selectedItem1, TDropListItem selectedItem2);
    
    // other methods elided
}

On the Controller side:

Here are the associated methods in the view's IModelControlAdapter.   Notice how the implementation of the view and its adapter is typed to the actual type of factories that to be used.

     // other code elided.
         
     _view = new BallGUI<IStrategyFac>(new IModelControlAdapter<IStrategyFac> () {  
             
             // Other methods elided.
             
             
             @Override
             /**
             * Returns an IStrategyFac that can instantiate the strategy specified
             * by classname. Returns null if classname is null. The toString() of
             * the returned factory is the classname.
             * @param classname  Shortened name of desired strategy 
             * @return A factory to make that strategy
             */
            public IStrategyFac addStrategy(String classname) {
                return _model.makeStrategyFac(classname);
            }

            @Override
            /**
             * Add a ball to the system with a strategy asgiven by the given IStrategyFac
             * @param selectedItem The fully qualified name of the desired strategy.
             */
            public void makeBall(IStrategyFac selectedItem) {
                if (null != selectedItem)
                    _model.loadBall(selectedItem.make());  // Here, loadBall takes a strategy object, but your method may take the strategy factory instead.

            }

            @Override
            /**
             * Returns an IStrategyFac that can instantiate a MultiStrategy with the
             * two strategies made by the two given IStrategyFac objects. Returns
             * null if either supplied factory is null. The toString() of the
             * returned factory is the toString()'s of the two given factories,
             * concatenated with "-".             * 
             * @param selectedItem1 An IStrategyFac for a strategy
             * @param selectedItem2 An IStrategyFac for a strategy
             * @return An IStrategyFac for the composition of the two strategies
             */
            public IStrategyFac combineStrategies(IStrategyFac selectedItem1, IStrategyFac selectedItem2) {
                return _model.combineStrategyFacs(selectedItem1, selectedItem2);
            }
            
            // etc.
        }, 
        new IModelUpdateAdapter() {
            /**
            * Pass the update request to the model.
            * @param g The Graphics object the balls use to draw themselves.
            */
            public void update(Graphics g) {
                _model.update(g);
            }
        });
            

Notice that the GUI only deals with Objects, not IStrategyFac's.  In fact, the GUI never knows about the existence of the factories.   The job of the GUI is to specify that selections for adding and combining have been made, not what those choices actually do or how they do it.   Hence, IStrategyFac is an interface completely outside the realm of the view.

 

The Take-Away:

The key to proper design here was the recognition that the system could be expressed in terms of an abstract construction process.

 

An interesting viewpoint:   Can you see that the factories create a binary tree?  The Add to lists button creates a leaf and the Combine! button creates a non-empty binary tree whose children are either leaves or non-empty trees. Actually constructing a strategy object requires traversing the tree to find all of its pieces, but because the tree is defined recursively, that traversal is both transparent and automatic!   Gotta love it!

 

For more specifics on completing the composition-based Ballworld project, please see the HW03 web page.



© 2017 by Stephen Wong