rwaldhoff 2003/03/28 17:14:22 Modified: functor/src/test/org/apache/commons/functor/example FlexiMapExample.java Log: adding commentary Revision Changes Path 1.5 +245 -29 jakarta-commons-sandbox/functor/src/test/org/apache/commons/functor/example/FlexiMapExample.java Index: FlexiMapExample.java =================================================================== RCS file: /home/cvs/jakarta-commons-sandbox/functor/src/test/org/apache/commons/functor/example/FlexiMapExample.java,v retrieving revision 1.4 retrieving revision 1.5 diff -u -r1.4 -r1.5 --- FlexiMapExample.java 5 Mar 2003 01:12:47 -0000 1.4 +++ FlexiMapExample.java 29 Mar 2003 01:14:22 -0000 1.5 @@ -81,6 +81,20 @@ import org.apache.commons.functor.core.RightIdentityFunction; import org.apache.commons.functor.core.composite.ConditionalUnaryFunction; +/* + * ---------------------------------------------------------------------------- + * INTRODUCTION: + * ---------------------------------------------------------------------------- + */ + +/* + * In this example, we'll demonstrate how we can use "pluggable" functors + * to create specialized Map implementations via composition. + * + * All our specializations will use the same basic Map implementation. + * Once it is built, we'll only need to define the specialized behaviors. + */ + /** * @version $Revision$ $Date$ * @author Rodney Waldhoff @@ -95,19 +109,46 @@ return new TestSuite(FlexiMapExample.class); } + /* + * ---------------------------------------------------------------------------- + * UNIT TESTS: + * ---------------------------------------------------------------------------- + */ + + /* + * In a "test first" style, let's first specify the Map behaviour we'd like + * to implement via unit tests. + */ + + /* + * First, let's review the basic Map functionality. + */ + + /* + * The basic Map interface lets one associate keys and values: + */ public void testBasicMap() { - Map map = makeBasicMap(); + /* (We'll define these make*Map functions below.) */ + Map map = makeBasicMap(); Object key = "key"; Object value = new Integer(3); map.put(key,value); assertEquals(value, map.get(key) ); } + /* + * If there is no value associated with a key, + * the basic Map will return null for that key: + */ public void testBasicMapReturnsNullForMissingKey() { Map map = makeBasicMap(); assertNull( map.get("key") ); } + /* + * One can also explicitly store a null value for + * some key: + */ public void testBasicMapAllowsNull() { Map map = makeBasicMap(); Object key = "key"; @@ -116,6 +157,10 @@ assertNull( map.get(key) ); } + /* + * The basic Map deals with Objects--it can store keys + * and values of multiple or differing types: + */ public void testBasicMapAllowsMultipleTypes() { Map map = makeBasicMap(); map.put("key-1","value-1"); @@ -129,6 +174,11 @@ assertEquals(new Integer(4), map.get(new Integer(4)) ); } + /* + * Finally, note that putting a second value for a given + * key will overwrite the first value--the basic Map only + * stores the most recently put value for each key: + */ public void testBasicMapStoresOnlyOneValuePerKey() { Map map = makeBasicMap(); @@ -138,7 +188,14 @@ assertEquals("value-2", map.get("key") ); } + /* + * Now let's look at some specializations of the Map behavior. + */ + /* + * One common specialization is to forbid null values, + * like our old friend Hashtable: + */ public void testForbidNull() { Map map = makeNullForbiddenMap(); @@ -152,14 +209,32 @@ } } + /* + * Alternatively, we may want to provide a default + * value to return when null is associated with some + * key. (This might be useful, for example, when the Map + * contains a counter--when there's no count yet, we'll + * want to treat it as zero.): + */ public void testNullDefaultsToZero() { - Map map = makeNullAsZeroMap(); + Map map = makeDefaultValueForNullMap(new Integer(0)); + /* + * We expect 0 when no value has been associated with "key". + */ + assertEquals( new Integer(0), map.get("key") ); + /* + * We also expect 0 when a null value has been associated with "key". + */ map.put("key", null); assertEquals( new Integer(0), map.get("key") ); } + /* + * Another common specialization is to constrain the type of values + * that may be stored in the Map: + */ public void testIntegerValuesOnly() { - Map map = makeIntegerValuedMap(); + Map map = makeTypeConstrainedMap(Integer.class); map.put("key", new Integer(2)); assertEquals( new Integer(2), map.get("key") ); try { @@ -170,6 +245,15 @@ } } + /* + * A more interesting specialization is that used by the + * Jakarta Commons Collections MultiMap class, which allows + * one to associate multiple values with each key. The put + * function still accepts a single value, but the get function + * will return a Collection of values. Associating multiple values + * with a key adds to that collection, rather than overwriting the + * previous value: + */ public void testMultiMap() { Map map = makeMultiMap(); @@ -204,6 +288,13 @@ } + /* + * Here's another variation on the MultiMap theme. + * Rather than adding elements to a Collection, let's + * concatenate String values together, delimited by commas. + * (Such a Map might be used by the Commons Collection's + * ExtendedProperties type.): + */ public void testStringConcatMap() { Map map = makeStringConcatMap(); map.put("key", "value 1"); @@ -214,8 +305,28 @@ assertEquals("value 1, value 2, value 3",map.get("key")); } + /* + * ---------------------------------------------------------------------------- + * THE GENERIC MAP IMPLEMENTATION: + * ---------------------------------------------------------------------------- + */ + + /* + * How can one Map implementation support all these behaviors? + * Using functors and composition, of course. + * + * In order to keep our example small, we'll just consider the + * primary Map.put and Map.get methods here, although the remaining + * Map methods could be handled similiarly. + */ static class FlexiMap implements Map { + /* + * Our FlexiMap will accept two BinaryFunctions, one + * that's used to transform objects being put into the Map, + * and one that's used to transforms objects being retrieved + * from the map. + */ public FlexiMap(BinaryFunction putfn, BinaryFunction getfn) { if(null == putfn) { onPut = new RightIdentityFunction(); @@ -231,17 +342,36 @@ proxiedMap = new HashMap(); } + + /* + * The arguments to our "onGet" function will be the + * key and the value associated with that key in the + * underlying Map. We'll return whatever the function + * returns. + */ + public Object get(Object key) { + return onGet.evaluate( key, proxiedMap.get(key) ); + } + + /* + * The arguments to our "onPut" function will be the + * value previously associated with that key (if any), + * as well as the new value being associated with that key. + * + * Since put returns the previously associated value, + * we'll invoke onGet here as well. + */ public Object put(Object key, Object value) { Object oldvalue = proxiedMap.get(key); proxiedMap.put(key, onPut.evaluate(oldvalue, value)); return onGet.evaluate(key,oldvalue); } - public Object get(Object key) { - return onGet.evaluate( key, proxiedMap.get(key) ); - } - + /* + * We'll skip the remaining Map methods for now. + */ + public void clear() { throw new UnsupportedOperationException("Left as an exercise for the reader."); } @@ -287,16 +417,46 @@ private Map proxiedMap = null; } + /* + * ---------------------------------------------------------------------------- + * MAP SPECIALIZATIONS: + * ---------------------------------------------------------------------------- + */ + + /* + * For the "basic" Map, we'll simply create a HashMap. + * Note that using a RightIdentityFunction for onPut and onGet + * would yield the same behavior. + */ private Map makeBasicMap() { return new HashMap(); } + /* + * To prohibit null values, we'll only need to + * provide an onPut function. + */ private Map makeNullForbiddenMap() { return new FlexiMap( - IgnoreLeftFunction.adapt( + /* + * We simply ignore the left-hand argument, + */ + IgnoreLeftFunction.adapt( + /* + * and for the right-hand, + */ new ConditionalUnaryFunction( + /* + * we'll test for null, + */ IsNull.getIsNullPredicate(), + /* + * throwing a NullPointerException when the value is null, + */ UnaryProcedureUnaryFunction.adapt(throwNPE), + /* + * and passing through all non-null values. + */ IdentityFunction.getIdentityFunction() ) ), @@ -304,25 +464,61 @@ ); } - private Map makeNullAsZeroMap() { + /* + * To provide a default for null values, we'll only need to + * provide an onGet function, simliar to the onPut method used + * above. + */ + private Map makeDefaultValueForNullMap(Object defaultValue) { return new FlexiMap( + null, + /* + * We ignore the left-hand argument, + */ IgnoreLeftFunction.adapt( + /* + * and for the right-hand, + */ new ConditionalUnaryFunction( + /* + * we'll test for null, + */ IsNull.getIsNullPredicate(), - new ConstantFunction(new Integer(0)), + /* + * returning our default when the value is otherwise null, + */ + new ConstantFunction(defaultValue), + /* + * and passing through all non-null values. + */ IdentityFunction.getIdentityFunction() ) - ), - null + ) ); } - private Map makeIntegerValuedMap() { + /* + * To constrain the value types, we'll + * provide an onPut function, + */ + private Map makeTypeConstrainedMap(Class clazz) { return new FlexiMap( + /* + * ignore the left-hand argument, + */ IgnoreLeftFunction.adapt( new ConditionalUnaryFunction( - new IsInstanceOf(Integer.class), + /* + * we'll test the type of the right-hand argument, + */ + new IsInstanceOf(clazz), + /* + * and either pass the given value through, + */ IdentityFunction.getIdentityFunction(), + /* + * or throw a ClassCastException. + */ UnaryProcedureUnaryFunction.adapt(throwCCE) ) ), @@ -330,6 +526,11 @@ ); } + /* + * The MultiMap is a bit more interesting, since we'll + * need to consider both the old and new values during + * onPut: + */ private Map makeMultiMap() { return new FlexiMap( new BinaryFunction() { @@ -348,8 +549,15 @@ ); } + /* + * The StringConcatMap is more interesting still. + */ private Map makeStringConcatMap() { return new FlexiMap( + /* + * The onPut function looks similiar to the MultiMap + * method: + */ new BinaryFunction() { public Object evaluate(Object oldval, Object newval) { StringBuffer buf = null; @@ -363,6 +571,10 @@ return buf; } }, + /* + * but we'll also need an onGet functor to convert + * the StringBuffer to a String: + */ new BinaryFunction() { public Object evaluate(Object key, Object val) { if(null == val) { @@ -375,29 +587,33 @@ ); } - private interface UniversalProcedure extends Procedure, UnaryProcedure, BinaryProcedure { } + /* + * (This "UniversalProcedure" type provides a procedure + * that takes the same action regardless of the number of + * parameters. We used it above to throw Exceptions when + * needed.) + */ + + private abstract class UniversalProcedure implements Procedure, UnaryProcedure, BinaryProcedure { + public abstract void run(); + + public void run(Object obj) { + run(); + } + public void run(Object left, Object right) { + run(); + } + } private UniversalProcedure throwNPE = new UniversalProcedure() { public void run() { throw new NullPointerException(); } - public void run(Object obj) { - run(); - } - public void run(Object left, Object right) { - run(); - } }; private UniversalProcedure throwCCE = new UniversalProcedure() { public void run() { throw new ClassCastException(); - } - public void run(Object obj) { - run(); - } - public void run(Object left, Object right) { - run(); } };
--------------------------------------------------------------------- To unsubscribe, e-mail: [EMAIL PROTECTED] For additional commands, e-mail: [EMAIL PROTECTED]