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]