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