7
\$\begingroup\$

Now I have this fluent API for creating random integer arrays. I can say that I want an array of particular length, having a particular minimum/maximum values and using a particular (or default) java.util.Random. See what I have:

FluentArrays.java:

package net.coderodde.util;

import java.util.Arrays;
import java.util.Random;

/**
 * This class facilitates creation of random integer arrays using a fluent API.
 * 
 * @author Rodion "rodde" Efremov
 * @version 1.6 (Sep 11, 2015)
 */
public class FluentArrays {

    /**
     * This inner static class is responsible for choosing between integers and
     * long integers.
     */
    public static class TypeSelector {

        /**
         * Chooses long integers and returns the array length selector.
         * 
         * @return the array length selector. 
         */
        public LongLengthSelector ofLongs() {
            return new LongLengthSelector();
        }

        /**
         * Chooses integers and returns the array length selector.
         * 
         * @return the array length selector. 
         */
        public IntLengthSelector ofIntegers() {
            return new IntLengthSelector();
        }
    }

    /**
     * This inner static class is responsible for choosing the length of a long
     * integer array.
     */
    public static class LongLengthSelector {

        /**
         * Chooses the array length and returns the minimum value selector.
         * 
         * @param  length the length of the array.
         * @return the minimum value selector.
         */
        public LongMinimumSelector ofLength(int length) {
            checkLength(length);
            return new LongMinimumSelector(length);
        }
    }

    /**
     * This inner static class is responsible for choosing the length of an 
     * integer array.
     */
    public static class IntLengthSelector {

        /**
         * Chooses the array length and returns the minimum value selector.
         * 
         * @param  length the length of the array.
         * @return the minimum value selector.
         */
        public IntMinimumSelector ofLength(int length) {
            checkLength(length);
            return new IntMinimumSelector(length);
        }
    }

    /**
     * This inner static class is responsible for choosing the minimum value of
     * an array of long integers.
     */
    public static class LongMinimumSelector {

        /**
         * The length of the requested array.
         */
        private final int length;

        private LongMinimumSelector(int length) {
            this.length = length;
        }

        /**
         * Selects the value of the minimum array component.
         * 
         * @param  minimum the minimum value allowed.
         * @return the maximum value selector.
         */
        public LongMaximumSelector withMinimum(long minimum) {
            return new LongMaximumSelector(length, minimum);
        }
    }

    /**
     * This inner static class is responsible for choosing the minimum value of
     * an array of integers.
     */
    public static class IntMinimumSelector {

        /**
         * The length of the requested array.
         */
        private final int length;

        private IntMinimumSelector(int length) {
            this.length = length;
        }

        /**
         * Selects the value of the minimum array component.
         * 
         * @param  minimum the minimum value allowed.
         * @return the maximum value selector.
         */
        public IntMaximumSelector withMinimum(int minimum) {
            return new IntMaximumSelector(length, minimum);
        }
    }

    /**
     * This inner static class is responsible for choosing the maximum value of
     * an array of long integers.
     */
    public static class LongMaximumSelector {

        /**
         * The length of the requested array.
         */
        private final int length;

        /**
         * The minimum requested value.
         */
        private final long minimum;

        private LongMaximumSelector(int length, long minimum) {
            this.length = length;
            this.minimum = minimum;
        }

        /**
         * Selects the value of the maximum array component.
         * 
         * @param  maximum the maximum value allowed.
         * @return the random number generator selector.
         */
        public LongRandomSelector withMaximum(long maximum) {
            checkMinMax(minimum, maximum);
            return new LongRandomSelector(length, minimum, maximum);
        }
    }

    /**
     * This inner static class is responsible for choosing the maximum value of
     * an array of integers.
     */
    public static class IntMaximumSelector {

        /**
         * The length of the requested array.
         */
        private final int length;

        /**
         * The minimum requested value.
         */
        private final int minimum;

        private IntMaximumSelector(int length, int minimum) {
            this.length = length;
            this.minimum = minimum;
        }

        /**
         * Selects the value of the maximum array component.
         * 
         * @param  maximum the maximum value allowed.
         * @return the random number generator selector.
         */
        public IntRandomSelector withMaximum(int maximum) {
            checkMinMax(minimum, maximum);
            return new IntRandomSelector(length, minimum, maximum);
        }
    }

    /**
     * This inner static class is responsible for creating the actual long 
     * integer arrays.
     */
    public static class LongRandomSelector {

        /**
         * The length of the requested array.
         */
        private final int length;

        /**
         * The minimum requested value.
         */
        private final long minimum;

        /**
         * The maximum requested value.
         */
        private final long maximum;

        private LongRandomSelector(int length,
                                   long minimum,
                                   long maximum) {
            this.length = length;
            this.minimum = minimum;
            this.maximum = maximum;
        }

        /**
         * Creates the actual array of long integers using a specified random
         * number generator.
         * 
         * @param  random the random number generator.
         * @return the array of long integers.
         */
        public long[] withRandom(Random random) {
            final long[] array = new long[length];

            for (int i = 0; i < length; ++i) {
                array[i] = Math.abs(random.nextLong()) % 
                           (maximum - minimum + 1) + minimum;
            }

            return array;
        }

        /**
         * Create the actual array of long integers using a default random 
         * number generator.
         * 
         * @return the array of long integers.
         */
        public long[] withDefaultRandom() {
            return withRandom(new Random());
        }
    }

    /**
     * This inner static class is responsible for creating the actual integer
     * arrays.
     */
    public static class IntRandomSelector {

        /**
         * The length of the requested array.
         */
        private final int length;

        /**
         * The minimum requested value.
         */
        private final int minimum;

        /**
         * The maximum requested value.
         */
        private final int maximum;

        private IntRandomSelector(int length,
                                  int minimum,
                                  int maximum) {
            this.length = length;
            this.minimum = minimum;
            this.maximum = maximum;
        }

        /**
         * Creates the actual array of integers using a specified random number
         * generator.
         * 
         * @param  random the random number generator.
         * @return the array of integers.
         */
        public int[] withRandom(Random random) {
            final int[] array = new int[length];

            for (int i = 0; i < length; ++i) {
                array[i] = random.nextInt(maximum - minimum + 1) + minimum;
            }

            return array;
        }

        /**
         * Creates the actual array of integers using a default random number 
         * generator.
         * 
         * @return the array of integers.
         */
        public int[] withDefaultRandom() {
            return withRandom(new Random());
        }
    }

    /**
     * This class is responsible for initiating a fluent API call.
     * 
     * @return the type selector.
     */
    public static TypeSelector createArray() {
        return new TypeSelector();
    }

    private static void checkLength(int length) {
        if (length < 0) {
            throw new IllegalArgumentException(
                    "The requested length is negative (" + length + ").");
        }
    }

    private static void checkMinMax(int a, int b) {
        if (a > b) {
            throw new IllegalArgumentException(
                    "The requested minimum value " +
                    "(" + a + ") is greater than the maximum (" + b + ").");
        }
    }

    private static void checkMinMax(long a, long b) {
        if (a > b) {
            throw new IllegalArgumentException(
                    "The requested minimum value " +
                    "(" + a + ") is greater than the maximum (" + b + ").");
        }
    }

    public static void main(String[] args) {
        long[] array1 = createArray().ofLongs()
                                     .ofLength(12)
                                     .withMinimum(-100L)
                                     .withMaximum(200L)
                                     .withDefaultRandom();

        System.out.println(Arrays.toString(array1));

        int[] array2 = createArray().ofIntegers()
                                    .ofLength(15)
                                    .withMinimum(-10)
                                    .withMaximum(20)
                                    .withDefaultRandom();

        System.out.println(Arrays.toString(array2));
    }
}

So, what do you think?

\$\endgroup\$
  • 1
    \$\begingroup\$ Have you considered using generics ? \$\endgroup\$ – Spotted Sep 11 '15 at 9:35
  • \$\begingroup\$ Does your API needed to be designed in a way that all parameters (type, length, min, max, random) are mandatory ? \$\endgroup\$ – Spotted Sep 11 '15 at 9:43
  • \$\begingroup\$ It can be refactored so that it does not ask for minimum/maximum elements and, thus, generates all possible values, yet I wanted more control over the generated data. \$\endgroup\$ – coderodde Sep 11 '15 at 9:46
  • 2
    \$\begingroup\$ @Spotted - how would generics help? Odd question. \$\endgroup\$ – rolfl Sep 11 '15 at 10:29
  • \$\begingroup\$ @rolfl I remarked that all selectors have the same code (except specifiying long/int), so it could reduce code duplication. \$\endgroup\$ – Spotted Sep 11 '15 at 11:06
7
\$\begingroup\$

Ouch. That's a lot of code to do things that are already available in the native Java libraries. Also, while the usage code "looks" neat, writing it is a PITA because it returns a different object at each step, and is not really a fluent API at all because the order of method calls is pre-defined. StringBuilder is an example where append(...) returns the same instance so you can keep appending and so on.... that's "fluent".

Regardless, your code is complicated to write, it's inconvenient to use because you need to write a lot of code to link a number of highly structured method calls that are in an inflexible call order.

long[] array1 = createArray().ofLongs()
                 .ofLength(12)
                 .withMinimum(-100L)
                 .withMaximum(200L)
                 .withDefaultRandom();

The above example code, for me, would be written better, as:

long[] array = ThreadLocalRandom.longs(12, -100, 200).toArray();

If you want a non-default random, you can just use that instance:

Random rand = new Random();
long[] more = rand.longs(12, -100, 200).toArray();

The Stream API is more than enough when it comes to "fluent".

Also, if you don't want to include the size in the top call, you can limit the stream too:

long[] more = rand.longs(-100, 200).limit(12).toArray();

Of course, you can do the same with ints.... (and doubles).

int[] ints = rand.ints(-100, 200).limit(12).toArray();
\$\endgroup\$
  • \$\begingroup\$ Yes, good point about predefined order, but I think people should differentiate between "strong" fluent APIs (whose order is predefined) and "weak" fluent APIs that do not impose any order, or even demand particular attribute. However, one of the reasons why people do fluent APIs is that code editors nowadays do code completion, so such an IDE can "guide" the client programmer through the process of making a fluent API call, right? \$\endgroup\$ – coderodde Sep 11 '15 at 10:51
  • \$\begingroup\$ Also I have seen people calling strong fluent APIs "fluent APIs". \$\endgroup\$ – coderodde Sep 11 '15 at 10:52
  • \$\begingroup\$ That's a discussion I would rather avoid. I don't like any API's (fluent, or not) that make things harder, not easier. Your API makes a trivial thing hard. \$\endgroup\$ – rolfl Sep 11 '15 at 10:57
  • 1
    \$\begingroup\$ I don't think that it is a review what you are doing. It's called discouraging. \$\endgroup\$ – coderodde Sep 11 '15 at 11:16
  • 2
    \$\begingroup\$ @coderodde Many times when writing code, what you end up writing might not be as good as something that already exists. Learn from the things that already exists. Also see Reviewing usefulness of code and Is "You're doing it wrong" a review?. You asked the question "So, what do you think?", and you got an answer to that here. If you want, you can open a discussion on meta or chat with both rolfl and I in The 2nd Monitor \$\endgroup\$ – Simon Forsberg Sep 11 '15 at 12:24
4
\$\begingroup\$

I think you've overcomplicated things for yourself. A fluent API on top of @rolfl's suggested alternative may have a merit, if it's fluent in a more traditional sense.

In particular, I think the builder pattern can be useful here. You could have a builder static inner class that gathers all the parameters such as min, max, default or custom random, number of elements, at each step returning a builder instance, until a final toIntArray or toLongArray call, validating the accumulated parameters. The implementation will be a lot simpler, and there will be no disputes about strong/weak fluency.

The builder could also keep default parameters, such as the random object to use, so that users don't need to specify explicitly.

I would also shorten some of the method names, for example withMin instead of withMinimum. Sure, an IDE auto completes just the same, but it's shorter to read and the meaning is just as obvious.

\$\endgroup\$
1
\$\begingroup\$

I think this code is a very good example of how to write fluent APIs, even though there exists far easier ways to get an certain array.

flaws of the fluent interface:

you should let the the user be in choice in having the order of min/max createArray().ofIntegers().ofLength(15).withMinimum(-10).withMaximum(20); createArray().ofIntegers().ofLength(15).withMaximum(20).withMinimum(-10);

The idea of the fluent interface is indeed to guide the user input. That's done very well with your code, but sometimes you have to provide a loose order. This is harder to implement but makes the user-experience wayyys better. Since your code is an academical question, I would be happy to see such an implementation!!!

\$\endgroup\$
  • 1
    \$\begingroup\$ Thanks! I will review this post when I retire. :-) \$\endgroup\$ – coderodde Feb 18 at 15:06
  • \$\begingroup\$ @coderodde i didn't think that anyone EVER would read this *gg \$\endgroup\$ – Martin Frank Feb 19 at 6:01
  • 1
    \$\begingroup\$ My retirement may happen soon. \$\endgroup\$ – coderodde Feb 19 at 6:06

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.