Cartesian Plane Lesson 18 Page 2: Refactoring

Refactoring, LinePropertySet, GraphPropertySet, ColorEditor, FontEditor, equals, hashCode

On this page, we will look backward and tweak some of our existing code to better support the new functionality we’re adding in this lesson. We will be modifying the following facilities:

The Utils Class
We are adding a method to the Utils class on the test source branch to delete a file directory recursively.

GraphPropertySet and LinePropertySet
To facilitate the upcoming development and testing of the new Profile class, we will add an equals method to the various GraphPropertySets and LinePropertySets. Since we are adding equals methods we are obligated to add customized hashCode methods as well. Fortuitously, the data for all the GraphPropertySet concrete subclasses are contained in the abstract GraphPropertySet, so we need only add a single equals/hashCode method pair to the GraphPropertySet class. Likewise, the data for all the LinePropertySet concrete subclasses are contained in the abstract LinePropertySet, so we can add a single equals/hashCode method pair to the LinePropertySet class. Of course, since we are adding new functionality to these classes, we will also need to update their corresponding JUnit test classes.

ColorEditor class
FontEditor class
Significant components in these classes will be given component names to facilitate testing.

GitHub repository: Cartesian Plane Part 18

Previous lesson: Cartesian Plane Lesson 18 Page 1: A Final Look at Properties

The Utils Class

The Utils class on the test source branch has a new utility for recursively deleting a directory. Recursion is necessary because a directory cannot be deleted while it contains files. To delete the directory, we first have to delete all its files. And if one of those files is a directory, all its files must be deleted, etc. Here’s the new method:

public static void recursiveDelete( File file )
{
    if ( file.isDirectory() )
        Stream.of( file.listFiles() )
        .forEach( Utils::recursiveDelete );
    file.delete();
}

The LinePropertySet Class

Here is a listing for the hashCode and equals methods for the LinePropertySet class. We assume that none of the fields will ever be null, so there is no need to test for null values. The methods are quite straightforward, and I don’t believe much discussion is warranted. Note: the hashCode and equals methods go into the abstract LinePropertySet class and satisfy the requirements for all concrete subclasses.

@Override
public int hashCode()
{
    int hashCode    = Objects.hash(
        drawProperty ,
        strokeProperty,
        lengthProperty,
        spacingProperty,
        colorProperty,
        draw,
        stroke,
        length,
        spacing,
        color
    );
}

@Override
public boolean equals( Object other )
{
    boolean result  = false;
    if ( other == null )
        result = false;
    else if ( this == other )
        result = true;
    else if ( this.getClass() != other.getClass() )
        result = false;
    else
    {
        LinePropertySet that    = (LinePropertySet)other;
        result = 
            this.draw.equals( that.draw )
            && this.stroke.equals( that.stroke )
            && this.length.equals( that.length )
            && this.spacing.equals( that.spacing )
            && this.color.equals(that.color );
    }
    return result;
}

The GraphPropertySet Class

Here is a listing for the hashCode and equals methods for the GraphPropertySet class. We assume that none of the fields will ever be null, so there is no need to test for null values. The methods are quite straightforward, and I don’t believe much discussion is warranted. Note: the hashCode and equals methods go into the abstract GraphPropertySet class and satisfy the requirements for all concrete subclasses.

@Override
public int hashCode()
{
    int hashCode    = Objects.hash(
        widthProperty ,
        bgColorProperty,
        fgColorProperty,
        fontNameProperty,
        fontSizeProperty,
        fontStyleProperty,
        fontDrawProperty,
        width,
        bgColor,
        fgColor,
        fontName,
        fontSize,
        fontStyle,
        fontDraw
    );
    return hashCode;
}

@Override
public boolean equals( Object other )
{
    boolean result  = false;
    if ( other == null )
        result = false;
    else if ( this == other )
        result = true;
    else if ( this.getClass() != other.getClass() )
        result = false;
    else
    {
        GraphPropertySet    that    = (GraphPropertySet)other;
        result = 
            this.width == that.width
            && this.fgColor.equals( that.fgColor )
            && this.bgColor.equals( that.bgColor )
            && this.fontName.equals( that.fontName )
            && this.isBold() == that.isBold()
            && this.isItalic() == that.isItalic()
            && this.fontSize == that.fontSize
            && this.fontDraw == that.fontDraw;

    }
    return result;
}

Refactoring the JUnit Test Classes

Since we added equals and hashCode methods to the LinePropertySet and GraphPropetySet classes, we need to update our JUnit test classes to test the new methods. We’ll discuss these updates next.

LinePropertySetTest

Updating the LinePropertySetTest class required us to add a public method to test the equals and hashCode methods and two helper methods to support it.

⬛ Helper Methods

Following is a discussion of the helper methods we added to the LinePropertySet test class.

🟦 private static LinePropertySet copy( LinePropertySet src )
This method makes a copy of a given LinePropertySet object. Its primary purpose is to support testing the equals and hashCode methods. Immediately after creation, the original object and the copy can be tested for equality, and we can verify that they return the same hashcode. Then, a change can be made to a single field of one of the objects, and the two objects can be tested for inequality. Here is a listing of the copy method.

private LinePropertySet copy( LinePropertySet src )
{
    LinePropertySet dest    = setSupplier.get();
    dest.setDraw( src.getDraw() );
    dest.setStroke( src.getStroke() );
    dest.setLength( src.getLength() );
    dest.setSpacing( src.getSpacing() );
    dest.setColor( src.getColor() );

    return dest;
}

🟦 private void testEqualsByField( LinePropertySet left, Consumer<LinePropertySet> mutator )
This method is invoked by the public testEquals method once for each supported field in a concrete LinePropertySet subclass. The caller provides a LinePropertySet object to test against and a mutator that changes the value of a single field in an object of the given type (LinePropertySetAxes, LinePropertySetGridLines, etc.). The helper method copies the given object and tests it against the original for equality. It then uses the mutator to modify the copy and verifies that it no longer equals the original. An example of invoking this method might be:

LinePropertySet left            = setSupplier.get();
LinePropertySet uniqueValues    = copy( left );
getUniqueValues( uniqueValues );

Boolean diffVal = uniqueValues.getDraw();
testEqualsByField( left, s -> s.setDraw( diffVal ) );

Here is an annotated listing of this method.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
private void testEqualsByField( 
    LinePropertySet left, 
    Consumer<LinePropertySet> mutator
)
{
    LinePropertySet right   = copy( left );
    assertTrue( left.equals( left ) );
    assertFalse( left.equals( null ) );
    assertFalse( left.equals( new Object() ) );
    assertTrue( left.equals( right ) );
    assertTrue( right.equals( left ) );
    assertEquals( left.hashCode(), right.hashCode() );
    
    mutator.accept( right );
    assertFalse( left.equals( right ) );
    assertFalse( right.equals( left ) );
}
  • Line 6: Make a copy of the input LinePropertySet.
  • Line 7: Verify reflexivity of equals.
  • Line 8: Verify the equals(null) returns false (non-nullity property).
  • Line 9: Verify logic when the argument is not a compatible class.
  • Line 10: Verify that equals on an equivalent object returns true,
  • Line 11: Verify symmetricity.
  • Line 12: Verify that equal objects return equal hashcodes.
  • Line 14: Change one property of one object to a unique value.
  • Line 15: Verify that the objects are no longer equal.
  • Line 16: Verify symmetricity.

⏹ Public Methods
We have one public test method that validates the equals and hashCode methods.

🟦 public void testEquals()
This method invokes the testEqualsByField helper method for each property supported by a given LinePropertySet subclass. For example, testEqualsByField will be invoked for the spacing property of the LinePropertySetTicMajor class but not for the LinePropertySetAxes class. A listing of this method follows.

@Test
public void testEquals()
{
    LinePropertySet left            = setSupplier.get();
    LinePropertySet uniqueValues    = copy( left );
    getUniqueValues( uniqueValues );

    if ( left.hasDraw() )
    {
        Boolean diffVal = uniqueValues.getDraw();
        testEqualsByField( left, s -> s.setDraw( diffVal ) );
    }
    if ( left.hasStroke() )
    {
        float   diffVal = uniqueValues.getStroke();
        testEqualsByField( left, s -> s.setStroke( diffVal ) );
    }
    if ( left.hasSpacing() )
    {
        float   diffVal = uniqueValues.getSpacing();
        testEqualsByField( left, s -> s.setSpacing( diffVal ) );
    }
    if ( left.hasColor() )
    {
        Color   diffVal = uniqueValues.getColor();
        testEqualsByField( left, s -> s.setColor( diffVal ) );
    }
}

GraphPropertySetTest

Testing the equals and hashCode methods in the GraphPropertySet class is essentially equivalent to the testing in the LinePropertySetTest class. Minor differences are due to the way GraphPropertySetTest manages its test data. Specifically, it stores three different GraphPropertySet objects as instance variables:

private final GraphPropertySet              origValues;
private final GraphPropertySet              newValues;
private final Supplier<GraphPropertySet>    setSupplier;

private GraphPropertySet    testSet;
  • origValues is initialized in the setOrigValues methods, which, as much as possible, gives each property a distinct value; for example, the bold property is set to true, and the italic property is set to false; the foreground color property is set to blue, and the background property to red.
  • newValues consists of property values calculated to be distinct from those stored in origValues. This object is initialized in the calcNewValues method.
  • testSet is initialized at the start of each test to property values equivalent to those stored in origValues. It may be modified as needed during the execution of a test.

Two other factors account for the differences between refactoring LinePropertySetTest and GraphPropertySetTest:

  • GraphPropertySetTest already has a copy method, so we didn’t have to write a new one.
  • Properties in the GraphPropertySet class are never optional in a subclass. That is to say, every GraphPropertySet subclass supports every GraphPropertySet property, unlike LinePropertySetTicMajor and LinePropertySetAxes, the first of which supports the draw property while the second does not.

Like the LinePropertySetTest class, the GraphPropertySetTest class splits the validation of the equals and hashCode methods between the public testEquals method and the testEqualsByField helper method. Following is a listing of these two methods. See also testEquals and testEqualsByField in the LinePropertySetTest class.

private void testEqualsByField( 
    Consumer<GraphPropertySet> mutator
)
{
    copy( testSet, origValues );
    assertTrue( origValues.equals( origValues ) );
    assertFalse( origValues.equals( null ) );
    assertFalse( origValues.equals( new Object() ) );
    assertTrue( origValues.equals( testSet ) );
    assertTrue( testSet.equals( origValues ) );
    assertEquals( origValues.hashCode(), testSet.hashCode() );
    
    mutator.accept( testSet );
    assertFalse( origValues.equals( testSet ) );
    assertFalse( testSet.equals( origValues ) );
}
@Test
public void testEquals()
{
    setOrigValues();
    calcNewValues();
    testEqualsByField( s -> s.setWidth( newValues.getWidth() ) );
    testEqualsByField( s -> s.setBGColor( newValues.getBGColor() ) );
    testEqualsByField( s -> s.setFGColor( newValues.getFGColor() ) );
    testEqualsByField( s -> s.setFontName( newValues.getFontName() ) );
    testEqualsByField( s -> s.setFontSize( newValues.getFontSize() ) );
    testEqualsByField( s -> s.setBold( newValues.isBold() ) );
    testEqualsByField( s -> s.setItalic( newValues.isItalic() ) );
    testEqualsByField( s -> s.setFontDraw( newValues.isFontDraw() ) );
}

ColorEditor Class

After refactoring, any component in the ColorEditor GUI that we might want to refer to in a test will have a component name. Each name is encapsulated in a public constant, for example:
    public static final String COLOR_BUTTON_LABEL = "Color";
    // ...
    colorButton.setName( COLOR_BUTTON_LABEL );

Now, if we want to use ComponentFinder to find the pushbutton in the ColorEditor that brings up the color chooser, we can use code like this:
    String                name = ColorEditor.COLOR_BUTTON_LABEL;
    Predicate<JComponent
> pred = jc -> name.equals( jc.getName() );
    JComponent            comp = ComponentFinder.find( source, pred );

The figure below illustrates which names/public constants are assigned to which components in the ColorEditor:

Color Editor Component Names

See also ProfileEditorTestBase. See also the diff file ColorEditor17 vs ColorEditor18, which highlights the differences between this lesson’s ColorEditor.java file and the previous version.*

FontEditor Class

After refactoring, any component in the FontEditor class that we might want to refer to in a test will have a component name. Each name is encapsulated in a public constant, for example:
    public static final String BOLD_LABEL = "Bold";
    // ...
    boldToggle.setName( BOLD_LABEL );

Now, if we want to use ComponentFinder to find the checkbox in the FontEditor that controls the bold property, we can use code like this:
    String                name = FontEditor.BOLD_LABEL;
    Predicate<JComponent> pred = jc -> name.equals( jc.getName() );
    JComponent            comp = ComponentFinder.find( source, pred );

The figure below illustrates which names/public constants are assigned to which components in the FontEditor; the Color button and color text editor are not labeled because those components are borrowed from the ColorEditor class and have names assigned via the ColorEditor (see ColorEditor Class above):

See also ProfileEditorTestBase. See also the diff file FontEditor17 vs FontEditor18, which highlights the differences between this lesson’s FontEditor.java file and the previous version.*

*This diff was generated by Vim, a free and open-source text editor initially developed by Bram Molenaar.

Summary

On this page, we added a new method to the Utils class on the test source path and equals methods to the LinePropertySet and GraphPropertySet abstract classes; adding the equals methods required us to add customized hashCode methods. To assist with testing, we also added component names to some of the components in the FontEditor and ColorEditor classes. Next, we’ll develop the new Profile class.

Next: Profiles