Appendix: The Shapes demo code listing

This chapter consists purely of the code listing for the Shapes demo. Please check out the Shapes demo mini site as it provides the source code and a range of other reports and information.

The Shapes Demo class diagram
The Shapes Demo class diagram

package org.groovy_tutorial.shapes

TwoDimensionalShape

package org.groovy_tutorial.shapes

/**
 * An interface for basic two-dimensional objects
 *
 * @see <a href="https://en.wikipedia.org/wiki/List_of_two-dimensional_geometric\
_shapes">
 *     Wikipedia: List of two-dimensional geometric shapes</a>
 *
 * @author Duncan Dickinson
 */
interface TwoDimensionalShape {

    /**
     * The length of the path surrounding a 2D shape
     * @see <a href="https://en.wikipedia.org/wiki/Perimeter">Wikipedia: Perimet\
er</a>
     * @return the perimeter of the shape
     */
    BigDecimal getPerimeter()

    /**
     * The extent of a 2D shape in a plane
     * @see <a href="https://en.wikipedia.org/wiki/Area">Wikipedia: Area</a>
     * @return the area of the shape
     */
    BigDecimal getArea()

    /**
     * A handy display string
     * @return a text representation of the shape
     */
    String getDisplayInfo()

    /**
     * @return the name of the shape
     */
    String getShapeName()
}

ShapeUtil

package org.groovy_tutorial.shapes

/**
 * A general utility class
 *
 * @author Duncan Dickinson
 */
class ShapeUtil {

    /**
     * The shapes library supports sides of length > 0. This method helps check \
this.
     * @param sides a series of parameters, each reflecting a side's length
     * @return true if all sides are valid, false otherwise
     */
    static boolean checkSides(Number... sides) {
        for (side in sides) {
            if (side <= 0) {
                return false
            }
        }
        true
    }

    /**
     * Helper method - throws an exception if checkSides returns false
     * @param sides a series of parameters, each reflecting a side's length
     * @throws IllegalArgumentException if one of the sides <= 0
     */
    static void checkSidesException(Number... sides) throws IllegalArgumentExcep\
tion {
        if (!checkSides(sides)) {
            throw new IllegalArgumentException('The side must be a positive numb\
er.')
        }
    }
}

Sides

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")
    }
}

Circle

package org.groovy_tutorial.shapes

import static java.lang.Math.PI

import groovy.transform.EqualsAndHashCode
import groovy.transform.ToString

/**
 * Describes a circle
 * @author Duncan Dickinson
 */
@EqualsAndHashCode(includes = 'radius')
@ToString(includeNames = true, includeFields = true, includePackage = true)
final class Circle implements TwoDimensionalShape {
    private static final String SHAPE_NAME = 'Circle'

    /** The radius of the circle */
    final BigDecimal radius

    /** The circle's perimeter (circumference) */
    final BigDecimal perimeter

    /** The circle's area */
    final BigDecimal area

    /**
     *
     * @param radius the radius of the circle (must be a positive number)
     * @throws IllegalArgumentException if radius <= 0
     */
    Circle(BigDecimal radius) throws IllegalArgumentException {
        ShapeUtil.checkSidesException(radius)
        this.radius = radius
        this.perimeter = calculatePerimeter(radius)
        this.area = calculateArea(radius)
    }

    /**
     * Helper function - defers to calculatePerimeter
     * @see #calculatePerimeter(Number)
     * @param radius
     * @return the circumference (perimeter)
     * @throws IllegalArgumentException if radius <= 0
     */
    static BigDecimal calculateCircumference(Number radius) throws IllegalArgume\
ntException {
        calculatePerimeter(radius)
    }

    /**
     * Calculates the perimeter of a circle using the formula: p = 2*Pi*r
     * @param radius
     * @return the perimeter
     * @throws IllegalArgumentException if radius <= 0
     */
    static BigDecimal calculatePerimeter(Number radius) throws IllegalArgumentEx\
ception {
        ShapeUtil.checkSidesException(radius)
        (2 * PI * radius) as BigDecimal
    }

    /**
     * Calculates the area of a circle using the formula: a = Pi*r^2
     * @param radius
     * @return the area
     * @throws IllegalArgumentException if radius <= 0
     */
    static BigDecimal calculateArea(Number radius) throws IllegalArgumentExcepti\
on {
        ShapeUtil.checkSidesException(radius)
        (PI * radius**2) as BigDecimal
    }

    /**
     * Calculates the circle's diameter using the formula: d = 2r
     * @param radius
     * @return the diameter
     * @throws IllegalArgumentException if radius <= 0
     */
    static BigDecimal calculateDiameter(Number radius) throws IllegalArgumentExc\
eption {
        ShapeUtil.checkSidesException(radius)
        (radius * 2) as BigDecimal
    }

    @Override
    String getDisplayInfo() {
        "$SHAPE_NAME: radius = $radius; diameter = $diameter; \
circumference = ${circumference}; area = ${area}"
    }

    /**
     * Just a convenience - equivalent to getPerimeter
     * @return the circumference
     */
    BigDecimal getCircumference() {
        perimeter
    }

    /**
     * A pseudo getter
     * @return the diameter
     */
    Number getDiameter() {
        calculateDiameter(this.radius)
    }

    @Override
    String getShapeName() {
        SHAPE_NAME
    }
}

Rectangle

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
    }
}

Square

package org.groovy_tutorial.shapes

import groovy.transform.EqualsAndHashCode
import groovy.transform.ToString

/**
 * Describes a square
 *
 * @author Duncan Dickinson
 */
@EqualsAndHashCode(callSuper = true)
@ToString(includeNames = true, includeFields = true, includePackage = true, incl\
udeSuper = true)
final class Square extends Rectangle {

    private static final String SHAPE_NAME = 'Square'

    /**
     * @param length
     * @throws IllegalArgumentException if one of the sides <= 0
     */
    Square(Number length) throws IllegalArgumentException {
        super(length, length)
    }

    @Override
    String getDisplayInfo() {
        "$SHAPE_NAME: length = ${a}; perimeter = $perimeter; area = $area"
    }
}

Triangle

package org.groovy_tutorial.shapes

import groovy.transform.EqualsAndHashCode
import groovy.transform.ToString
import groovy.util.logging.Log

/**
 * Describes a generic triangle.
 *
 * For more specific types, use a class from {@link org.groovy_tutorial.shapes.t\
riangle}
 *
 * @author Duncan Dickinson
 */
@Log
@EqualsAndHashCode(includes = 'sideMap')
@ToString(includeNames = true, includeFields = true, includePackage = true)
class Triangle implements TwoDimensionalShape, Sides {
    static final String SHAPE_NAME = 'Triangle'

    final BigDecimal area

    /**
     * Configures the sides (a, b, c) of the triangle and calls the perimeter pr\
operty
     * of the Sides trait in order to make the sides (mostly) immutable and lock\
 in
     * the perimeter calculation
     *
     * The protected calculateArea method is called to determine the area of the
     * triangle. The result is assigned to the <code>area</code> field.
     *
     * @param a One of the triangle's three sides
     * @param b One of the triangle's three sides
     * @param c One of the triangle's three sides
     * @throws IllegalArgumentException if one of the sides <= 0
     */
    Triangle(Number a, Number b, Number c) throws IllegalArgumentException {
        this.a = a
        this.b = b
        this.c = c

        //Calling this causes the Sides trait to calculate the perimeter
        //and lock off its sideMap
        this.perimeter
        this.area = calculateArea()
    }

    /**
     * Determines the area of this triangle.
     * In this implementation we call the static calculateArea(a, b, c) method
     *
     * Subclasses can override this method if they feel they're able to provide a
     * leaner calculation.
     *
     * @return the area
     */
    protected BigDecimal calculateArea() {
        calculateArea(a, b, c)
    }

    /**
     * Uses Heron's formula to determine the area of the Triangle
     *
     * @see <a href="https://en.wikipedia.org/wiki/Heron%27s_formula">Wikipedia \
- Heron's Formula</a>
     * @throws IllegalArgumentException if a, b or c are <= 0
     */
    static final BigDecimal calculateArea(Number a, Number b, Number c) throws I\
llegalArgumentException {
        log.info "Triangle.calculateArea was called with a=$a, b=$b, c=$c"
        ShapeUtil.checkSidesException(a, b, c)
        Number s = (a + b + c) / 2
        Math.sqrt(s * (s - a) * (s - b) * (s - c))
    }

    @Override
    String getDisplayInfo() {
        "$SHAPE_NAME: Side A = $a; Side B = $b; \
Side C = $c; perimeter = $perimeter; area = $area"
    }

    @Override
    String getShapeName() {
        SHAPE_NAME
    }
}

package org.groovy_tutorial.shapes.triangle

TriangleSubtype

package org.groovy_tutorial.shapes.triangle

import groovy.transform.SelfType
import org.groovy_tutorial.shapes.Triangle

/**
 * A basic example of a targeted trait
 *
 * @author Duncan Dickinson
 */
@SelfType(Triangle)
trait TriangleSubtype {

    /**
     * Expects that implementations provide a value for the TRIANGLE_TYPE String
     * @return a String representing the type of triangle
     */
    String getTriangleType() {
        TRIANGLE_TYPE
    }
}

TriangleRightAngled

package org.groovy_tutorial.shapes.triangle

import groovy.transform.EqualsAndHashCode
import groovy.transform.ToString
import groovy.util.logging.Log
import org.groovy_tutorial.shapes.ShapeUtil
import org.groovy_tutorial.shapes.Triangle

/**
 * A triangle made famous by Pythagoras
 *
 * @author Duncan Dickinson
 */
@Log
@EqualsAndHashCode(callSuper = true)
@ToString(includeNames = true, includeFields = true, includePackage = true, incl\
udeSuper = true)
final class TriangleRightAngled extends Triangle implements TriangleSubtype {
    static final String TRIANGLE_TYPE = 'Right-angled'

    /**
     * Tell us the "other sides" and the hypotenuse will be determined for you!
     * @param a one of the "other sides"
     * @param b one of the "other sides"
     * @throws IllegalArgumentException if one of the sides <= 0
     */
    TriangleRightAngled(Number a, Number b) throws IllegalArgumentException {
        super(a, b, calculateHypotenuse(a, b))
    }

    /**
     * Determines the area of this triangle.
     * In this implementation we call the static calculateArea(a, b) method
     * @return the area
     */
    @Override
    protected BigDecimal calculateArea() {
        calculateArea(a, b)
    }

    /**
     * Determine a right-angled triangle's hypotenuse using Pythagoras' theorem
     * @param a
     * @param b
     * @return the hypotenuse
     * @throws IllegalArgumentException if a or b <= 0
     */
    static Number calculateHypotenuse(Number a, Number b) throws IllegalArgument\
Exception {
        ShapeUtil.checkSidesException(a, b)
        Math.sqrt(a**2 + b**2)
    }

    /**
     * Uses 0.5 * a * b
     * @param a
     * @param b
     * @return the area
     * @throws IllegalArgumentException if a or b <= 0
     */
    static Number calculateArea(Number a, Number b) throws IllegalArgumentExcept\
ion {
        log.info "TriangleRightAngled.calculateArea was called with a=$a, b=$b"
        ShapeUtil.checkSidesException(a, b)
        0.5 * a * b
    }

    @Override
    String getDisplayInfo() {
        "$TRIANGLE_TYPE ${super.displayInfo}"
    }
}

TriangleIsosceles

package org.groovy_tutorial.shapes.triangle

import groovy.transform.EqualsAndHashCode
import groovy.transform.ToString
import groovy.util.logging.Log
import org.groovy_tutorial.shapes.ShapeUtil
import org.groovy_tutorial.shapes.Triangle

/**
 * A triangle with two sides of equal length
 *
 * @see <a href="https://en.wikipedia.org/wiki/Isosceles_triangle">Wikipedia - I\
sosceles triangle</a>
 *
 * @author Duncan Dickinson
 */
@Log
@EqualsAndHashCode(callSuper = true)
@ToString(includeNames = true, includeFields = true, includePackage = true, incl\
udeSuper = true)
class TriangleIsosceles extends Triangle implements TriangleSubtype  {
    protected static String TRIANGLE_TYPE =  'Isosceles'

    /**
     * Create a triangle that has one base side and two equal sides (legs)
     * @param a the base
     * @param b the leg(s)
     * @throws IllegalArgumentException if one of the sides <= 0
     */
    TriangleIsosceles(Number base, Number leg) throws IllegalArgumentException {
        super(base, leg, leg)
    }

    /**
     * Determines the area of this triangle.
     * In this implementation we call the static calculateArea(a, b) method
     * @return
     */
    @Override
    protected BigDecimal calculateArea() {
        calculateArea(a, b)
    }

    /**
     * Calculates the area of an isosceles triangle using a simplified version o\
f Heron's formula
     * @param base the base
     * @param leg the leg(s)
     * @return
     * @see <a href="https://en.wikipedia.org/wiki/Isosceles_triangle#Area">Wiki\
pedia article</a>
     * @throws IllegalArgumentException if base or leg <= 0
     */
    static final BigDecimal calculateArea(Number base, Number leg) throws Illega\
lArgumentException {
        log.info "TriangleIsosceles.calculateArea was called with base=$base, b=\
$leg"
        ShapeUtil.checkSidesException(base, leg)
        def height = Math.sqrt(leg**2 - (base**2 / 4))
        base * (height / 2)
    }

    @Override
    String getDisplayInfo() {
        "$TRIANGLE_TYPE ${super.displayInfo}"
    }
}

TriangleEquilateral

package org.groovy_tutorial.shapes.triangle

import groovy.transform.EqualsAndHashCode
import groovy.transform.ToString
import groovy.util.logging.Log
import org.groovy_tutorial.shapes.ShapeUtil

/**
 * A triangle with three equal sides
 *
 * @see <a href="https://en.wikipedia.org/wiki/Equilateral_triangle">Wikipedia -\
 Equilateral triangle</a>
 *
 * @author Duncan Dickinson
 */
@Log
@EqualsAndHashCode(callSuper = true)
@ToString(includeNames = true, includeFields = true, includePackage = true, incl\
udeSuper = true)
final class TriangleEquilateral extends TriangleIsosceles {

    static final String TRIANGLE_TYPE = 'Equilateral'

    /**
     *
     * @param a
     * @throws IllegalArgumentException if a <= 0
     */
    TriangleEquilateral(Number a) throws IllegalArgumentException {
        super(a, a)
    }

    /**
     * Determines the area of this triangle.
     * In this implementation we call the static calculateArea(a) method
     * @return the area
     */
    @Override
    protected BigDecimal calculateArea() {
        calculateArea(a)
    }

    /**
     * Calculates the area of an equilateral triangle
     * @param a the edge (side) length
     * @return the area
     * @see <a href="https://en.wikipedia.org/wiki/Equilateral_triangle#Derivati\
on_of_area_formula">Wikipedia article</a>
     * @throws IllegalArgumentException if a <= 0
     */
    static BigDecimal calculateArea(Number a) throws IllegalArgumentException {
        log.info "TriangleEquilateral.calculateArea was called with a=$a"
        ShapeUtil.checkSidesException(a)
        (Math.sqrt(3) / 4) * a**2
    }

    @Override
    String getDisplayInfo() {
        "$TRIANGLE_TYPE ${super.displayInfo}"
    }
}

package org.groovy_tutorial.shapes.triangle

This package provides the basic command-line script (Main.groovy) for demonstrating the Shapes demo.

Main

package org.groovy_tutorial.shapes.app

import org.groovy_tutorial.shapes.Circle
import org.groovy_tutorial.shapes.Rectangle
import org.groovy_tutorial.shapes.Square
import org.groovy_tutorial.shapes.Triangle
import org.groovy_tutorial.shapes.triangle.TriangleEquilateral
import org.groovy_tutorial.shapes.triangle.TriangleIsosceles
import org.groovy_tutorial.shapes.triangle.TriangleRightAngled

def shapes = [
        new Rectangle(10, 2),
        new Square(4),
        new Circle(8),
        new Triangle(5, 8, 10),
        new TriangleRightAngled(3, 4),
        new TriangleIsosceles(2, 8),
        new TriangleEquilateral(6) ]

println """
${'=' * 80}

Welcome to the Shapes demo

${'=' * 80}
"""

shapes.each {
    println "${it.displayInfo}\n"
    //println "  - toString(): ${it.toString()}"
}