After reading Steve Yegge's post about the properties design pattern and how it can be used to create a prototype-based object system, I thought I'd have a go at an implementation in Java to understand more.
If you haven't read the above post already, it definitely worth a read (or two), and makes everything below make more sense, give it a go.
First, I wanted to define the minimum API needed to get a working properties object as described in the Overview section. A basic properties interface:
Properties.java
package org.adrianwalker.propertiespattern; interface Properties<N, V> { V get(N name); V put(N name, V value); boolean has(N name); V remove(N name); }
I'll defer describing the implementation until the end, because as soon as you have the basic functionality done there is a load more you're going to want to add.
Once you've got your Properties implementation storing name/value pairs and inheriting from a prototype Properties object stored against a reserved key name, at that point, your going to want to add some way for your objects to simulate methods to manipulate the property values.
Because Java doesn't have first class functions, a methods functionality needs to be encapsulated using the Strategy pattern.
Strategy.java
package org.adrianwalker.propertiespattern; interface Strategy<N, V, T, A> { T execute(Properties<N, V> properties, A... args); }
In addition to the Strategy interface, I extended the basic Properties interface with an execute method to call a Strategy property:
ExecutableProperites.java
package org.adrianwalker.propertiespattern; interface ExecutableProperites<N, V, T, A> extends Properties<N, V> { T execute(N name, A... args); }
You're also probably going to want a way of returning an object's property names, possibly filtered with by a regex, or a globing pattern, or partial string match or some kind object comparison. So another extension to the Properties interface:
FilterableProperties.java
package org.adrianwalker.propertiespattern; import java.util.Set; interface FilterableProperties<N, V> extends Properties<N, V> { Set<N> filter(N filter); }
And it would be good to have an interface for cloning prototype-based objects:
CloneableProperties.java
package org.adrianwalker.propertiespattern; interface CloneableProperties<N, V> extends Properties<N, V>, Cloneable { CloneableProperties<N, V> clone(); }
Implementation
With all of that out of the way it's time for an implementation. This implementation stores property name/value pairs in a HashMap
, and filters using regular expressions:
HashMapProperties.java
package org.adrianwalker.propertiespattern; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; import java.util.regex.Pattern; public final class HashMapProperties<V, T, A> implements Properties<String, V>, ExecutableProperites<String, V, T, A>, CloneableProperties<String, V>, FilterableProperties<String, V> { public static final String PROTOTYPE = "prototype"; private final Map<String, V> properties; public HashMapProperties() { this.properties = Collections.synchronizedMap(new HashMap()); } private HashMapProperties(final Map<String, V> map) { this.properties = Collections.synchronizedMap(new HashMap<String, V>(map)); } @Override public V get(final String name) { if (properties.containsKey(name)) { return properties.get(name); } if (properties.containsKey(PROTOTYPE)) { Properties<String, V> prototype = (Properties<String, V>) properties.get(PROTOTYPE); return prototype.get(name); } return null; } @Override public V put(final String name, final V value) { if (PROTOTYPE.equals(name) && !(value instanceof Properties)) { throw new IllegalArgumentException( "prototype must be an instance of Properties"); } return properties.put(name, value); } @Override public boolean has(final String name) { if (properties.containsKey(name)) { return null != properties.get(name); } if (properties.containsKey(PROTOTYPE)) { Properties<String, V> prototype = (Properties<String, V>) properties.get(PROTOTYPE); return prototype.has(name); } return false; } @Override public V remove(final String name) { if (properties.containsKey(PROTOTYPE)) { Properties<String, V> prototype = (Properties<String, V>) properties.get(PROTOTYPE); if (prototype.has(name)) { properties.put(name, null); return prototype.get(name); } } if (properties.containsKey(name)) { return properties.remove(name); } return null; } @Override public HashMapProperties<V, T, A> clone() { return new HashMapProperties<V, T, A>(properties); } @Override public T execute(final String name, final A... args) { V property = get(name); if (property instanceof Strategy) { return ((Strategy<String, V, T, A>) property).execute(this, args); } return null; } @Override public Set<String> filter(final String regex) { Pattern pattern = Pattern.compile(regex); Set<String> filteredNames = new HashSet<String>(); Set<String> names = properties.keySet(); synchronized (properties) { for (String name : names) { if (!PROTOTYPE.equals(name) && pattern.matcher(name).matches()) { filteredNames.add(name); } } } if (properties.containsKey(PROTOTYPE)) { FilterableProperties<String, V> prototype = (FilterableProperties<String, V>) properties.get(PROTOTYPE); filteredNames.addAll(prototype.filter(regex)); } return filteredNames; } }
Example Usage
The following example shows how you might use the above implementation. A prototype Properties object is created with a 'greeting' property and a 'getGreeting' Strategy property. These properties are inherited by a new Properties object.
The Properties object is cloned and it's greeting property is overridden.
The cloned Properties object's properties are filtered to return the getter Strategy's property name, then this is executed for the Properties object and it's clone:
Example.java
package org.adrianwalker.propertiespattern; import java.util.Set; import static org.adrianwalker.propertiespattern.HashMapProperties.PROTOTYPE; public final class Example { public static void main(final String[] args) { // create a prototype properties object with a greeting and a // strategy which returns the greeting and an argument Properties prototype = new HashMapProperties(); prototype.put("greeting", "Hello"); prototype.put("getGreeting", new Strategy() { @Override public String execute(final Properties properties, final Object... args) { return String.format("%s %s", properties.get("greeting"), args[0]); } }); // create a properties object which inherit the greeting and // strategy from the prototype HashMapProperties properties = new HashMapProperties(); properties.put(PROTOTYPE, prototype); // clone the properties object and override the greeting property HashMapProperties clone = properties.clone(); clone.put("greeting", "Ahoy"); // filter for the getter properties, then execute the getter // on each properties object with a message argument and // print the message Set<String> getters = clone.filter("get.*"); for (String getter : getters) { System.out.println(properties.execute(getter, "World")); System.out.println(clone.execute(getter, "Sailor")); } } }
The example above should print the output:
Hello World Ahoy Sailor
Finally
I've not implemented half of the functionality mentioned in original post, and not a fraction of a fraction of the possible applications for this pattern. The above code and a set of unit tests are available for download at the bottom of this page.
If you need the level of flexibility to warrant using this pattern, then, to quote the original article "use a programming language better suited for implementing the pattern: ideally, one that supports it from the ground up". So expect some JavaScript related posts in the future.
Source Code
- Properties-pattern Maven Project - properties-pattern.zip
Usage
Compile the project with 'mvn clean install
'.