102. The Shapes demo - Traits
The Sides trait is based on the notion that a two-dimensional shape consists of a set of sides (edges). In most cases there’d be at least 3 sides to a 2D shape (circles being the exception with 1 side) and it’s possible to determine a shape’s perimeter by adding up the lengths of the sides. In the Sides trait I wanted to provide classes with the ability to name each side using a single lower-case letter (e.g. a, b, c) and associate the side’s length.
Let’s take a look at the code for the Sides trait and then examine its components.
Sides traitpackage org.groovy_tutorial.shapes
/**
* A basic trait describing the outer edges (sides) of a 2D shape.
*
* This trait uses the missingProperty method to allow implementations
* to define sides using lower case characters (e.g. a, b, c). These properties \
must:
* <ul>
* <li>Be a single, lower-case character (matching Sides.SIDE_NAME_PATTERN)<\
/li>
* <li>Be only assigned a positive numeric value</li>
* </ul>
*
* Once getPerimeter is called, the sides map is locked down and can't be modifi\
ed.
*
* @author Duncan Dickinson
*/
trait Sides {
/** Defines the acceptable naming strategy for sides */
static final SIDE_NAME_PATTERN = /[a-z]/
/** Used to hold the named list of sides */
private final Map sideMap = [ : ]
/** The perimeter, as determined by the sum of the sides */
private BigDecimal perimeter = null
/**
* Calculates the perimeter of the shape (once).
* After calling this method, the sides are locked down and you can't add or\
edit them
* via propertyMissing
* @return the sum of the sideMap (the perimeter)
*/
BigDecimal getPerimeter() {
perimeter = perimeter ?:sideMap.values().sum().toBigDecimal()
}
/**
* @return a CLONE of the sideMap
*/
Map getSideMap() {
sideMap.clone() as Map
}
/**
* Gets the value for a named side
* @param name the name of the side (e.g. a, b, c)
* @return the value of the side
* @throws MissingPropertyException if name not found
*/
def propertyMissing(String name) throws MissingPropertyException {
if (name.matches(SIDE_NAME_PATTERN)) {
return sideMap.get(name)
}
throw new MissingPropertyException("Property $name not found")
}
/**
* Sets the length (value) for a named side
* @param name the name of the side
* @param value the length of the side
* @return the value back to the caller
* @throws ReadOnlyPropertyException if the perimeter has been calculated
* @throws IllegalArgumentException if the value <= 0
* @throws MissingPropertyException if name doesn't match SIDE_NAME_PATTERN
*/
def propertyMissing(String name, value)
throws ReadOnlyPropertyException, IllegalArgumentException, MissingP\
ropertyException {
if (name.matches(SIDE_NAME_PATTERN)) {
if (perimeter) {
throw new ReadOnlyPropertyException(name, Sides)
}
if (value in Number) {
ShapeUtil.checkSidesException(value)
sideMap.put(name, value as Number)
return sideMap.get(name)
}
throw new IllegalArgumentException("The value [$value] is not a posi\
tive number.")
}
throw new MissingPropertyException("Property $name not found")
}
}
Reviewing the code you’ll see:
- Each side will be added to the
sideMapwith a lower-case letter as the key and the side’s length as the value- The
SIDE_NAME_PATTERNprovides a very basic pattern to limit the acceptable keys - The
getSideMap()will return a clone1 ofsideMap- this helps protect the property from changing externally to the trait.
- The
- The
perimeterfield will hold the perimeter of the shape- This is calculated via the
getPerimeter()method (more on this in a moment) - Note how the perimeter is calculated only once
- This is calculated via the
Aside from the items listed above, you’ll notice two versions of the propertyMissing method. This is a special Groovy method that is called when a getter or setter is called on a property that doesn’t exist. The propertyMissing(String name) is called when code attempts to access (get) a non-existent property and propertyMissing(String name, value) is called when an attempt is made to mutate (set) a non-existent property. The getter is reasonably straight-forward as it just checks that the requested property name matches the SIDE_NAME_PATTERN and, if so, tries to access the property from sideMap.
The setter version of propertyMissing is a little more complex and, stepping through the method, we can see:
- The requested property
namemust matchSIDE_NAME_PATTERN - If the
perimeterhas already been calculated we throw an exception assideMapis locked down onceperimeterhas been set - The
valuefor the side (it’s length) must be aNumber - A utility method
ShapeUtil.checkSidesExceptionis called to ensure thatvalue > 0as we don’t want negative- or zero-length sides - Once all of those preconditions are met the property can be set
All of this results in the Sides trait providing implementing classes with not only the ability to store a list of sides and calculate the perimeter but also lets them use a nice letter-based notation for the sides.
Both the Triangle and Rectangle classes implement the Sides trait as well as the TwoDimensionalShape interface. By implementing Sides, these classes are provided with an implementation of the getPerimeter() method required by the TwoDimensionalShape interface.
We can see the interaction between the a shape class and the Sides trait by examining the Rectangle class:
Rectangle classpackage org.groovy_tutorial.shapes
import groovy.transform.EqualsAndHashCode
import groovy.transform.ToString
/**
* Describes a rectangle
*
* @author Duncan Dickinson
*/
@EqualsAndHashCode(includes = 'length,width')
@ToString(includeNames = true, includeFields = true, includePackage = true)
class Rectangle implements TwoDimensionalShape, Sides {
private static final String SHAPE_NAME = 'Rectangle'
/** The area of the rectangle */
final BigDecimal area
/**
*
* @param length
* @param width
* @throws IllegalArgumentException if one of the sides <= 0
*/
Rectangle(Number length, Number width) throws IllegalArgumentException {
a = length
b = width
c = length
d = width
//Calling this causes the Sides trait to calculate the perimeter
//and lock off its sideMap
this.perimeter
this.area = length * width
}
@Override
String getDisplayInfo() {
"$SHAPE_NAME: length = $a; width = $b; perimeter = $perimeter; area = $a\
rea"
}
@Override
String getShapeName() {
SHAPE_NAME
}
}
Most of Rectangle’s use of the trait is seen in the constructor as we set the sides of the rectangle though a really easy-to-understand notation:
a = length
b = width
c = length
d = width
The use of the Sides trait means that instances of Rectangle can use notation such as myRectangle.a.
The Rectangle constructor also calls this.perimeter so as to calculate the perimeter - not because we specifically need it in the constructor but because it locks down the set of sides for the rectangle instance.
- Cloning was mentioned previously↩