payne
payne

Reputation: 5289

Repetitive Java constructors (refactoring or generation)

Problematic

I've been making a library for a framework that I'm using in Java: I have hundreds of LOC used purely for providing many constructors to the users, but each time, with one less argument (because a default value can be used). That leads to a lot of repeated code, and a lot of repeated JavaDoc. And, most of all, a lot of maintenance whenever a little thing changes.


Example of solution

I feel like there could exist some library out there, similar to Lombok, that would allow me to annotate the constructors in such a way that it would automatically generate the constructors and their associated JavaDoc. Something like:

/** Some description. */
public class Example {    

    /** Alternate1 description. */
    public SomeObject alternate1;

    /** Alternate2 description. */
    public SomeOtherObject alternate2;

    /** Attribute1 description. */
    public int attribute1 = 1;

    /** Attribute2 description. */
    public int attribute2 = 2;

    @InjectClassDoc
    @InjectAttributesDoc
    public Example (@ConstructorRequiredVariant SomeObject obj,
            @ConstructorOrderedCascade int attribute1,
            @ConstructorOrderedCascade int attribute2) {
        doSomething();
    }

    @InjectClassDoc
    @InjectAttributesDoc
    public Example (@ConstructorRequiredVariant SomeOtherObject obj,
            @ConstructorOrderedCascade int attribute1,
            @ConstructorOrderedCascade int attribute2) {
        doSomething();
    }

    // getters and setters

}

That would generate something like:

/** Some description. */
public class Example {    

    /** Alternate1 description. */
    public SomeObject someObject;

    /** Alternate2 description. */
    public SomeOtherObject someOtherObject;

    /** Attribute1 description. */
    public int attribute1 = 1;

    /** Attribute2 description. */
    public int attribute2 = 2;

    /**
     * Some description.
     * @param obj Alternate1 description.
     * @param attribute1 Attribute1 description.
     * @param attribute2 Attribute2 description.
     */
    public Example (final SomeObject obj,
            final int attribute1,
            final int attribute2) {
        this(obj, attribute1);
        setAttribute2(attribute2);
    }

    /**
     * Some description.
     * @param obj Alternate1 description.
     * @param attribute1 Attribute1 description.
     */
    public Example (final SomeObject obj,
            final int attribute1) {
        this(obj);
        setAttribute1(attribute1);
    }

    /**
     * Some description.
     * @param obj Alternate1 description.
     */
    public Example (final SomeObject obj) {
        setSomeObject(obj);
        doSomething();
    }

    /**
     * Some description.
     * @param obj Alternate2 description.
     * @param attribute1 Attribute1 description.
     * @param attribute2 Attribute2 description.
     */
    public Example (final SomeOtherObject obj,
            final int attribute1,
            final int attribute2) {
        this(obj, attribute1);
        setAttribute2(attribute2);
    }

    /**
     * Some description.
     * @param obj Alternate2 description.
     * @param attribute1 Attribute1 description.
     */
    public Example (final SomeOtherObject obj,
            final int attribute1) {
        this(obj);
        setAttribute1(attribute1);
    }

    /**
     * Some description.
     * @param obj Alternate2 description.
     */
    public Example (final SomeOtherObject obj) {
        setSomeOtherObject(obj);
        doSomething();
    }

    // getters and setters

}

Real-world example of the problem

This is just to give you a better idea of where I'm coming from with all of this.

Here is an excerpt of the source of my problem (link to the actual repo's source):

/** Some description that is reused on all constructors seen by the users. */
public class PieWidget extends RadialGroup {

    /** Used internally for the shared properties among constructors of RadialWidgets. */
    protected void constructorsCommon(TextureRegion whitePixel) {
        this.whitePixel = whitePixel;
    }


    /** Used internally for the shared properties among constructors of RadialWidgets. */
    protected PieWidget(final TextureRegion whitePixel, float preferredRadius) {
        super(preferredRadius);
        constructorsCommon(whitePixel);
    }


    /** Used internally for the shared properties among constructors of RadialWidgets. */
    protected PieWidget(final TextureRegion whitePixel,
                        float preferredRadius, float innerRadiusPercent) {
        super(preferredRadius, innerRadiusPercent);
        constructorsCommon(whitePixel);
    }


    /** Used internally for the shared properties among constructors of RadialWidgets. */
    protected PieWidget(final TextureRegion whitePixel,
                        float preferredRadius, float innerRadiusPercent, float startDegreesOffset) {
        super(preferredRadius, innerRadiusPercent, startDegreesOffset);
        constructorsCommon(whitePixel);
    }


    /** Used internally for the shared properties among constructors of RadialWidgets. */
    protected PieWidget(final TextureRegion whitePixel,
                        float preferredRadius, float innerRadiusPercent,
                        float startDegreesOffset, float totalDegreesDrawn) {
        super(preferredRadius, innerRadiusPercent, startDegreesOffset, totalDegreesDrawn);
        constructorsCommon(whitePixel);
    }


    /**
     * See {@link PieWidget} for a description.
     *
     * @param whitePixel a 1x1 white pixel.
     * @param style defines the way the widget looks like.
     * @param preferredRadius the {@link #preferredRadius} that defines the 
     *                        size of the widget.
     */
    public PieWidget(final TextureRegion whitePixel,
                     PieWidgetStyle style, float preferredRadius) {
        this(whitePixel, preferredRadius);
        setStyle(style);
    }


    /**
     * See {@link PieWidget} for a description.
     *
     * @param whitePixel a 1x1 white pixel.
     * @param style defines the way the widget looks like.
     * @param preferredRadius the {@link #preferredRadius} that defines the 
     *                        size of the widget.
     * @param innerRadiusPercent the {@link #innerRadiusPercent} that defines
     *                           the percentage of the radius that is cut off,
     *                           starting from the center of the widget.
     */
    public PieWidget(final TextureRegion whitePixel,
                     PieWidgetStyle style, float preferredRadius,
                     float innerRadiusPercent) {
        this(whitePixel, preferredRadius, innerRadiusPercent);
        setStyle(style);
    }


    /**
     * See {@link PieWidget} for a description.
     *
     * @param whitePixel a 1x1 white pixel.
     * @param style defines the way the widget looks like.
     * @param preferredRadius the {@link #preferredRadius} that defines the 
     *                        size of the widget.
     * @param innerRadiusPercent the {@link #innerRadiusPercent} that defines
     *                           the percentage of the radius that is cut off,
     *                           starting from the center of the widget.
     * @param startDegreesOffset the {@link #startDegreesOffset} that defines
     *                           how far from the origin the drawing begins.
     */
    public PieWidget(final TextureRegion whitePixel,
                     PieWidgetStyle style, float preferredRadius,
                     float innerRadiusPercent, float startDegreesOffset) {
        this(whitePixel, preferredRadius, innerRadiusPercent, startDegreesOffset);
        setStyle(style);
    }


    /**
     * See {@link PieWidget} for a description.
     *
     * @param whitePixel a 1x1 white pixel.
     * @param style defines the way the widget looks like.
     * @param preferredRadius the {@link #preferredRadius} that defines the 
     *                        size of the widget.
     * @param innerRadiusPercent the {@link #innerRadiusPercent} that defines
     *                           the percentage of the radius that is cut off,
     *                           starting from the center of the widget.
     * @param startDegreesOffset the {@link #startDegreesOffset} that defines
     *                           how far from the origin the drawing begins.
     * @param totalDegreesDrawn the {@link #totalDegreesDrawn} that defines how
     *                          many degrees the widget will span, starting from
     *                          its {@link #startDegreesOffset}.
     */
    public PieWidget(final TextureRegion whitePixel,
                     PieWidgetStyle style, float preferredRadius,
                     float innerRadiusPercent, float startDegreesOffset, float totalDegreesDrawn) {
        this(whitePixel, preferredRadius, innerRadiusPercent, startDegreesOffset, totalDegreesDrawn);
        setStyle(style);
    }


    /**
     * See {@link PieWidget} for a description.
     *
     * @param whitePixel a 1x1 white pixel.
     * @param skin defines the way the widget looks like.
     * @param preferredRadius the {@link #preferredRadius} that defines the 
     *                        size of the widget.
     */
    public PieWidget(final TextureRegion whitePixel,
                     Skin skin, float preferredRadius) {
        this(whitePixel, preferredRadius);
        setStyle(skin.get(PieWidgetStyle.class));
    }


    /**
     * See {@link PieWidget} for a description.
     *
     * @param whitePixel a 1x1 white pixel.
     * @param skin defines the way the widget looks like.
     * @param preferredRadius the {@link #preferredRadius} that defines the 
     *                        size of the widget.
     * @param innerRadiusPercent the {@link #innerRadiusPercent} that defines
     *                           the percentage of the radius that is cut off,
     *                           starting from the center of the widget.
     */
    public PieWidget(final TextureRegion whitePixel,
                     Skin skin, float preferredRadius,
                     float innerRadiusPercent) {
        this(whitePixel, preferredRadius, innerRadiusPercent);
        setStyle(skin.get(PieWidgetStyle.class));
    }


    /**
     * See {@link PieWidget} for a description.
     *
     * @param whitePixel a 1x1 white pixel.
     * @param skin defines the way the widget looks like.
     * @param preferredRadius the {@link #preferredRadius} that defines the 
     *                        size of the widget.
     * @param innerRadiusPercent the {@link #innerRadiusPercent} that defines
     *                           the percentage of the radius that is cut off,
     *                           starting from the center of the widget.
     * @param startDegreesOffset the {@link #startDegreesOffset} that defines
     *                           how far from the origin the drawing begins.
     */
    public PieWidget(final TextureRegion whitePixel,
                     Skin skin, float preferredRadius,
                     float innerRadiusPercent, float startDegreesOffset) {
        this(whitePixel, preferredRadius, innerRadiusPercent, startDegreesOffset);
        setStyle(skin.get(PieWidgetStyle.class));
    }


    /**
     * See {@link PieWidget} for a description.
     *
     * @param whitePixel a 1x1 white pixel.
     * @param skin defines the way the widget looks like.
     * @param preferredRadius the {@link #preferredRadius} that defines the 
     *                        size of the widget.
     * @param innerRadiusPercent the {@link #innerRadiusPercent} that defines
     *                           the percentage of the radius that is cut off,
     *                           starting from the center of the widget.
     * @param startDegreesOffset the {@link #startDegreesOffset} that defines
     *                           how far from the origin the drawing begins.
     * @param totalDegreesDrawn the {@link #totalDegreesDrawn} that defines how
     *                          many degrees the widget will span, starting from
     *                          its {@link #startDegreesOffset}.
     */
    public PieWidget(final TextureRegion whitePixel,
                     Skin skin, float preferredRadius,
                     float innerRadiusPercent, float startDegreesOffset, float totalDegreesDrawn) {
        this(whitePixel, preferredRadius, innerRadiusPercent, startDegreesOffset, totalDegreesDrawn);
        setStyle(skin.get(PieWidgetStyle.class));
    }


    /**
     * See {@link PieWidget} for a description.
     *
     * @param whitePixel a 1x1 white pixel.
     * @param skin defines the way the widget looks like.
     * @param style the name of the style to be extracted from the skin.
     * @param preferredRadius the {@link #preferredRadius} that defines the 
     *                        size of the widget.
     */
    public PieWidget(final TextureRegion whitePixel,
                     Skin skin, String style, float preferredRadius) {
        this(whitePixel, preferredRadius);
        setStyle(skin.get(style, PieWidgetStyle.class));
    }




    /**
     * See {@link PieWidget} for a description.
     *
     * @param whitePixel a 1x1 white pixel.
     * @param skin defines the way the widget looks like.
     * @param style the name of the style to be extracted from the skin.
     * @param preferredRadius the {@link #preferredRadius} that defines the 
     *                        size of the widget.
     * @param innerRadiusPercent the {@link #innerRadiusPercent} that defines
     *                           the percentage of the radius that is cut off,
     *                           starting from the center of the widget.
     */
    public PieWidget(final TextureRegion whitePixel,
                     Skin skin, String style, float preferredRadius,
                     float innerRadiusPercent) {
        this(whitePixel, preferredRadius, innerRadiusPercent);
        setStyle(skin.get(style, PieWidgetStyle.class));
    }


    /**
     * See {@link PieWidget} for a description.
     *
     * @param whitePixel a 1x1 white pixel.
     * @param skin defines the way the widget looks like.
     * @param style the name of the style to be extracted from the skin.
     * @param preferredRadius the {@link #preferredRadius} that defines the 
     *                        size of the widget.
     * @param innerRadiusPercent the {@link #innerRadiusPercent} that defines
     *                           the percentage of the radius that is cut off,
     *                           starting from the center of the widget.
     * @param startDegreesOffset the {@link #startDegreesOffset} that defines
     *                           how far from the origin the drawing begins.
     */
    public PieWidget(final TextureRegion whitePixel,
                     Skin skin, String style, float preferredRadius,
                     float innerRadiusPercent, float startDegreesOffset) {
        this(whitePixel, preferredRadius, innerRadiusPercent, startDegreesOffset);
        setStyle(skin.get(style, PieWidgetStyle.class));
    }


    /**
     * See {@link PieWidget} for a description.
     *
     * @param whitePixel a 1x1 white pixel.
     * @param skin defines the way the widget looks like.
     * @param style the name of the style to be extracted from the skin.
     * @param preferredRadius the {@link #preferredRadius} that defines the 
     *                        size of the widget.
     * @param innerRadiusPercent the {@link #innerRadiusPercent} that defines
     *                           the percentage of the radius that is cut off,
     *                           starting from the center of the widget.
     * @param startDegreesOffset the {@link #startDegreesOffset} that defines
     *                           how far from the origin the drawing begins.
     * @param totalDegreesDrawn the {@link #totalDegreesDrawn} that defines how
     *                          many degrees the widget will span, starting from
     *                          its {@link #startDegreesOffset}.
     */
    public PieWidget(final TextureRegion whitePixel,
                     Skin skin, String style, float preferredRadius,
                     float innerRadiusPercent, float startDegreesOffset, float totalDegreesDrawn) {
        this(whitePixel, preferredRadius, innerRadiusPercent, startDegreesOffset, totalDegreesDrawn);
        setStyle(skin.get(style, PieWidgetStyle.class));
    }

    // ... then the actual methods start
}

The parent class:

public class RadialGroup extends WidgetGroup {

    /** Used internally for the shared properties among constructors of RadialWidgets. */
    protected void constructorsCommon() {
        setTouchable(Touchable.childrenOnly);
    }


    /**
     * See {@link RadialGroup} for a description.
     *
     * @param preferredRadius the {@link #preferredRadius} that defines the 
     *                        size of the widget.
     */
    public RadialGroup(float preferredRadius) {
        setPreferredRadius(preferredRadius);
        constructorsCommon();
    }

    /**
     * See {@link RadialGroup} for a description.
     *
     * @param preferredRadius the {@link #preferredRadius} that defines the 
     *                        size of the widget.
     * @param innerRadiusPercent the {@link #innerRadiusPercent} that defines
     *                           the percentage of the radius that is cut off,
     *                           starting from the center of the widget.
     */
    public RadialGroup(float preferredRadius, float innerRadiusPercent) {
        this(preferredRadius);
        setInnerRadiusPercent(innerRadiusPercent);
    }

    /**
     * See {@link RadialGroup} for a description.
     *
     * @param preferredRadius the {@link #preferredRadius} that defines the 
     *                        size of the widget.
     * @param innerRadiusPercent the {@link #innerRadiusPercent} that defines
     *                           the percentage of the radius that is cut off,
     *                           starting from the center of the widget.
     * @param startDegreesOffset the {@link #startDegreesOffset} that defines
     *                           how far from the origin the drawing begins.
     */
    public RadialGroup(float preferredRadius, float innerRadiusPercent,
                       float startDegreesOffset) {
        this(preferredRadius, innerRadiusPercent);
        setStartDegreesOffset(startDegreesOffset);
    }

    /**
     * See {@link RadialGroup} for a description.
     *
     * @param preferredRadius the {@link #preferredRadius} that defines the 
     *                        size of the widget.
     * @param innerRadiusPercent the {@link #innerRadiusPercent} that defines
     *                           the percentage of the radius that is cut off,
     *                           starting from the center of the widget.
     * @param startDegreesOffset the {@link #startDegreesOffset} that defines
     *                           how far from the origin the drawing begins.
     * @param totalDegreesDrawn the {@link #totalDegreesDrawn} that defines how
     *                          many degrees the widget will span, starting from
     *                          its {@link #startDegreesOffset}.
     */
    public RadialGroup(float preferredRadius, float innerRadiusPercent,
                       float startDegreesOffset, float totalDegreesDrawn) {
        this(preferredRadius, innerRadiusPercent, startDegreesOffset);
        setTotalDegreesDrawn(totalDegreesDrawn);
    }

    // ...
}

The structure is rather simple:

I'm wondering if there is a way to reduce that amount of copy-pasted code and documentation out there. Ideally, I would end up with only 3 constructors, instead of 12.

Upvotes: 0

Views: 87

Answers (1)

kaya3
kaya3

Reputation: 51142

To avoid writing exponentially many constructors when a class has several optional fields with default values, you can use the Builder pattern. For example:

public class Example {
    private int a;
    private String b;
    private char c;
    private boolean d;

    private Example(int a, String b, char c, boolean d) {
        this.a = a;
        this.b = b;
        this.c = c;
        this.d = d;
    }

    public static class Builder {
        // required fields
        private int a;
        private String b;

        // optional fields with default values
        private char c = 'c';
        private boolean d = false;

        public Builder(int a, String b) {
            this.a = a;
            this.b = b;
        }

        public Builder c(char c) {
            this.c = c;
            return this;
        }

        public Builder d(boolean d) {
            this.d = d;
            return this;
        }

        public Example build() {
            return new Example(a, b, c, d);
        }
    }
}

Then you can use it like new Example.Builder(23, "foo").c('y').d(false).build() to create a new object with every field given an initial value, or omit some of the chained methods to use the default values for those fields.

Upvotes: 1

Related Questions