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.

The Sides trait
package 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 sideMap with a lower-case letter as the key and the side’s length as the value
    • The SIDE_NAME_PATTERN provides a very basic pattern to limit the acceptable keys
    • The getSideMap() will return a clone1 of sideMap - this helps protect the property from changing externally to the trait.
  • The perimeter field 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

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:

  1. The requested property name must match SIDE_NAME_PATTERN
  2. If the perimeter has already been calculated we throw an exception as sideMap is locked down once perimeter has been set
  3. The value for the side (it’s length) must be a Number
  4. A utility method ShapeUtil.checkSidesException is called to ensure that value > 0 as we don’t want negative- or zero-length sides
  5. 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:

The Rectangle class
package 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.