13
\$\begingroup\$

Inspired by Extensible and testable FizzBuzz I have decided to write my own implementation, partially based on my own answer and on the other answers.

First a warning to all of you: Never make such complicated code for such simple jobs!

Now the basic requirements of the code:

  • Should be capable of solving the FizzBuzz problem.
  • Should be capable of solving the FizzBuzz problem in a generic way, with any number of divisors and FizzBuzz-values.
  • Should be able to count in a generic way, we see all original FizzBuzz statements where people only count over int. But that is not enough for us! With this solution we can count over any Number that is comparable. I'm aware of the performance hits of boxed int/long, but with Java Value Types and Generics over Value Types (including primitives) that should be a problem of the past in the future.

Then now the code will follow.

FizzBuzz

/**
 * Provides methods to convert numbers to their FizzBuzz-values given a set of rules.
 * 
 * <p>The FizzBuzz-value is a <tt>String</tt> obtained by applying a set of FizzBuzz-rules to a number.
 * An example of a FizzBuzz-rule is that a number that is divisible by a 3, must result in Fizz.
 * 
 * <p>If multiple FizzBuzz-values are obtained by applying multiple FizzBuzz-rules, then they will be joined together using an ordering and a delimiter, both the ordering and delimiter are configurable via the provided builder, respectively by providing a <tt>Comparator</tt> and providing a <tt>String</tt>.
 * An example of this is that in the classical FizzBuzz, the number 15 produces both <i>Fizz</i> and <i>Buzz</i>, with the default ordering and delimiter this will result in a FizzBuzz-value of <i>FizzBuzz</i>.
 * 
 * <p>This implementation only supports FizzBuzz-rules using divisibility.
 *
 * @author Frank van Heeswijk
 * @param <T>   The number used to count.
 */
public class FizzBuzz<T extends Number & Comparable<T>> {
    /** The sorted set of fizzers, to be used to calculate the FizzBuzz-value for every number. */
    private final SortedSet<FizzBuzzRule<T>> fizzBuzzRules;

    /** The increment operator, used to increment the number. */
    private final UnaryOperator<T> increment;

    /** The delimiter to be used when joining FizzBuzz values together. */
    private final String joinDelimiter;

    /**
     * Constructs a new FizzBuzz instance.
     * 
     * @param builder   The builder used to construct the new instance.
     */
    private FizzBuzz(final Builder<T> builder) {
        SortedSet<FizzBuzzRule<T>> tempFizzBuzzRules = new TreeSet<>(FizzBuzzRule.comparatorOnDivisor(builder.fizzBuzzRulesComparator));
        tempFizzBuzzRules.addAll(builder.fizzBuzzRules);
        this.fizzBuzzRules = Collections.unmodifiableSortedSet(tempFizzBuzzRules);
        this.increment = builder.increment;
        this.joinDelimiter = builder.joinDelimiter;
    }

    /**
     * Returns the FizzBuzz-value of a number.
     * 
     * @param number    The non-null number for which the FizzBuzz-value should be determined.
     * @return  The FizzBuzz-value of the number.
     * @throws NullPointerException If <tt>number</tt> is null.
     */
    public String getValue(final T number) {
        Objects.requireNonNull(number, "number");
        String output = fizzBuzzRules.stream()
            .flatMap(fizzer -> fizzer.nameIfCanDivide(number).map(Stream::of).orElseGet(Stream::empty))
            .collect(Collectors.joining(joinDelimiter));
        return (output.isEmpty() ? Objects.toString(number) : output);
    }

    /**
     * Returns a stream starting from a number.
     * 
     * <p>The user-supplied increment operator will be used to iterate from <tt>start</tt> onwards.
     * 
     * @param start The non-null number that is the inclusive starting point for the returned stream.
     * @return  A stream starting from the number.
     * @throws NullPointerException If <tt>start</tt> is null.
     */
    public Stream<String> rangeFrom(final T start) {
        Objects.requireNonNull(start, "start");
        return Stream.iterate(start, increment).map(this::getValue);
    }

    /**
     * Returns a builder that can be used to build FizzBuzz instances.
     * 
     * @param <T>   The number used to count.
     * @param increment The increment operator.
     * @param isDivisble    The predicate to check whether the <i>first</i> number is divisible by the <i>second</i> number.
     * @return  The builder instance.
     */
    public static <T extends Number & Comparable<T>> Builder<T> builder(final UnaryOperator<T> increment, final BiPredicate<T, T> isDivisble) {
        return new Builder<>(increment, isDivisble);
    }

    /**
     * Encapsulates a FizzBuzz-rule.
     * 
     * <p>It stores the FizzBuzz-value, divisor and "is divisible by predicate"-for a FizzBuzz-rule, and provides methods to work with them.
     * 
     * @param <T>   The number that is used by the FizzBuzz-rule.
     */
    private static class FizzBuzzRule<T extends Number & Comparable<T>> {
        /** The FizzBuzz-value of this FizzBuzz-rule. */
        private final String name;

        /** The divisor for this FizzBuzz-rule. */
        private final T divisor;

        /** The "is divisible by"-predicate for this FizzBuzz-rule. */
        private final BiPredicate<T, T> isDivisibleBy;

        /**
         * Constructs a new Fizzer instance.
         * 
         * @param name  The FizzBuzz-value of the FizzBuzz-rule.
         * @param divisor   The divisor number for this FizzBuzz-rule.
         * @param isDivisibleBy   The "is divisible by"-predicate for this FizzBuzz-rule.
         * @throws NullPointerException If <tt>name</tt>, <tt>divisor</tt> or <tt>isDivisibleBy</tt> is null.
         */
        private FizzBuzzRule(final String name, final T divisor, final BiPredicate<T, T> isDivisibleBy) {
            this.name = Objects.requireNonNull(name, "name");
            this.divisor = Objects.requireNonNull(divisor, "divisor");
            this.isDivisibleBy = Objects.requireNonNull(isDivisibleBy, "isDivisibleBy");
        }

        /**
         * Returns whether this FizzBuzz-rule can divide the given number.
         * 
         * <p>Returns true if and only if the given number is divisible by the divisor of this FizzBuzz-rule.
         * 
         * @param number    The non-null number to test divisibility for.
         * @return  Whether this FizzBuzz-rule can divide the given number.
         * @throws  NullPointerException    If <tt>number</tt> is null.
         */
        private boolean canDivide(final T number) {
            Objects.requireNonNull(number, "number");
            return isDivisibleBy.test(number, divisor);
        }

        /**
         * Returns an Optional, containing the FizzBuzz-value of this FizzBuzz-rule if and only if the given number is divisible by the divisor of this FizzBuzz-rule.
         * 
         * @param number    The non-null number to test divisiblity for.
         * @return  An <tt>Optional</tt>, containing the FizzBuzz-value of this FizzBuzz-rule if and only if the given number is divisible by the divisor of this FizzBuzz-rule.
         * @throws NullPointerException If <tt>number</tt> is null.
         */
        private Optional<String> nameIfCanDivide(final T number) {
            return (canDivide(number) ? Optional.of(name) : Optional.empty());
        }

        /**
         * Returns a Comparator on the divisor of this FizzBuzz-rule using a supplied Comparator that can be used on numbers.
         * 
         * @param <T>   The number that is used by this comparator.
         * @param comparator    The non-null comparator that can be used to compare numbers.
         * @return  A <tt>Comparator</tt> on the divisor of this FizzBuzz-rule using <tt>comparator</tt> that can be used on numbers.
         * @throws  NullPointerException    If <tt>comparator</tt> is null.
         */
        private static <T extends Number & Comparable<T>> Comparator<FizzBuzzRule<T>> comparatorOnDivisor(final Comparator<T> comparator) {
            Objects.requireNonNull(comparator, "comparator");
            return Comparator.comparing(fizzer -> fizzer.divisor, comparator);
        }
    }

    /**
     * A builder class that can be used to build FizzBuzz instances.
     * 
     * <p>The minimum requirement to build a FizzBuzz instance is to provide an increment operator and an "is divisible by"-predicate.
     * It is recommended that you add a number of name-divisor pairs that can then be added to the list of FizzBuzz-rules.
     * Optionally you can provide a custom comparator for the ordering of the FizzBuzz-rules and a custom join delimiter that is used when joining multiple FizzBuzz-values together.
     * 
     * <p>In a normal setup in the classical FizzBuzz example, the number 15 will be converted to <i>FizzBuzz</i>.
     * If you provide a reversed comparator and a join delimiter consisting of a single blank, then it will be converted to <i>Buzz Fizz</i>.
     * 
     * @param <T>   The number that will be used for counting.
     */
    public static class Builder<T extends Number & Comparable<T>> {
        /** The increment operator to be used by the FizzBuzz instances that will be built. */
        private final UnaryOperator<T> increment;

        /** The "is divisible by"-predicate to be used by the resulting FizzBuzz-rules. */
        private final BiPredicate<T, T> isDivisibleBy;

        /** The unordered set of FizzBuzz-rules. */
        private final Set<FizzBuzzRule<T>> fizzBuzzRules = new HashSet<>();

        /** The comparator to be used to order the set of FizzBuzz-rules. */
        private Comparator<T> fizzBuzzRulesComparator = Comparator.naturalOrder();

        /** The join delimiter to be used to join multiple FizzBuzz-values together. */
        private String joinDelimiter = "";

        /**
         * Constructs a new Builder instance that can be used to build FizzBuzz instances.
         * 
         * @param increment The non-null increment operator.
         * @param isDivisibleBy   The non-null "is divisible by"-predicate.
         * @throws NullPointerException If <tt>increment</tt> or <tt>isDivibleBy</tt> is null.
         */
        public Builder(final UnaryOperator<T> increment, final BiPredicate<T, T> isDivisibleBy) {
            this.increment = Objects.requireNonNull(increment, "increment");
            this.isDivisibleBy = Objects.requireNonNull(isDivisibleBy, "isDivisibleBy");
        }

        /**
         * Sets the comparator used by this builder.
         * 
         * @param comparator    The comparator used by this builder.
         * @return  This builder instance.
         * @throws NullPointerException If <tt>comparator</tt> is null.
         */
        public Builder<T> comparator(final Comparator<T> comparator) {
            this.fizzBuzzRulesComparator = Objects.requireNonNull(comparator, "comparator");
            return this;
        }

        /**
         * Sets the join delimiter used by this builder.
         * 
         * @param joinDelimiter The join delimiter used by this builder.
         * @return  This builder instance.
         * @throws NullPointerException If <tt>joinDelimiter</tt> is null.
         */
        public Builder<T> joinDelimiter(final String joinDelimiter) {
            this.joinDelimiter = Objects.requireNonNull(joinDelimiter, "joinDelimiter");
            return this;
        }

        /**
         * Adds a name-divisor pair to this builder.
         * 
         * <p>The name-divisor pair will be converted to a FizzBuzz-rule and added to the list of FizzBuzz-rules in this builder.
         * 
         * @param name  The FizzBuzz-value of a FizzBuzz-rule.
         * @param divisor   The divisor of a FizzBuzz-rule.
         * @return  This builder instance.
         * @throws IllegalStateException    If the supplied divisor is already in use in a different FizzBuzz-rule in this builder.
         * @throws NullPointerException If <tt>name</tt> or <tt>divisor</tt> is null.
         */
        public Builder<T> add(final String name, final T divisor) {
            if (fizzBuzzRules.stream().anyMatch(fizzBuzzRule -> fizzBuzzRule.divisor.equals(divisor))) {
                throw new IllegalStateException("The divisor " + divisor + " is already mapped");
            }
            fizzBuzzRules.add(new FizzBuzzRule<>(name, divisor, isDivisibleBy));
            return this;
        }

        /**
         * Builds a new FizzBuzz instance.
         * 
         * @return  A FizzBuzz instance produced by this builder.
         */
        public FizzBuzz<T> build() {
            return new FizzBuzz<>(this);
        }
    }
}

FizzBuzzDemo

/**
 *
 * @author Frank van Heeswijk
 */
public class FizzBuzzDemo {
    public static void main(String[] args) {
        FizzBuzz<Integer> fizzBuzzInteger = FizzBuzz.<Integer>builder(i -> i + 1, 
            (number, divisor) -> number % divisor == 0)
            .add("Fizz", 3)
            .add("Buzz", 5)
            .build();
        fizzBuzzInteger.rangeFrom(1).limit(100).forEach(System.out::println);

        System.out.println();

        FizzBuzz<BigInteger> fizzBuzzBigInteger = FizzBuzz.<BigInteger>builder(bi -> bi.add(BigInteger.ONE), 
            (number, divisor) -> number.mod(divisor).compareTo(BigInteger.ZERO) == 0)
            .comparator(Comparator.reverseOrder())
            .joinDelimiter(" ")
            .add("Fizz", BigInteger.valueOf(3))
            .add("Buzz", BigInteger.valueOf(5))
            .build();
        fizzBuzzBigInteger.rangeFrom(BigInteger.ONE).limit(100).forEach(System.out::println);
    }
}

FizzBuzzBuilderTest

/**
 *
 * @author Frank van Heeswijk
 */
public class FizzBuzzBuilderTest {
    @Test
    public void testConstructor() {
        FizzBuzz.Builder<Integer> builder = new FizzBuzz.Builder<>(i -> i + 1, (i, j) -> i % j == 0);
    }

    @Test(expected = NullPointerException.class)
    public void testConstructorNullIncrement() {
        FizzBuzz.Builder<Integer> builder = new FizzBuzz.Builder<>(null, (i, j) -> i % j == 0);
    }

    @Test(expected = NullPointerException.class)
    public void testConstructorNullIsDivisibleBy() {
        FizzBuzz.Builder<Integer> builder = new FizzBuzz.Builder<>(i -> i + 1, null);
    }

    @Test
    public void testComparator() {
        FizzBuzz.Builder<Integer> builder = new FizzBuzz.Builder<>(i -> i + 1, (i, j) -> i % j == 0);
        assertEquals(builder, builder.comparator(Comparator.naturalOrder()));
    }

    @Test(expected = NullPointerException.class)
    public void testComparatorNullComparator() {
        FizzBuzz.Builder<Integer> builder = new FizzBuzz.Builder<>(i -> i + 1, (i, j) -> i % j == 0);
        builder.comparator(null);
    }


    @Test
    public void testJoinDelimiter() {
        FizzBuzz.Builder<Integer> builder = new FizzBuzz.Builder<>(i -> i + 1, (i, j) -> i % j == 0);
        builder.joinDelimiter("");
    }

    @Test(expected = NullPointerException.class)
    public void testJoinDelimiterNullJoinDelimiter() {
        FizzBuzz.Builder<Integer> builder = new FizzBuzz.Builder<>(i -> i + 1, (i, j) -> i % j == 0);
        assertEquals(builder, builder.joinDelimiter(null));
    }

    @Test
    public void testAdd() {
        FizzBuzz.Builder<Integer> builder = new FizzBuzz.Builder<>(i -> i + 1, (i, j) -> i % j == 0);
        assertEquals(builder, builder.add("Fizz", 3).add("Buzz", 5));
    }

    @Test(expected = IllegalStateException.class)
    public void testAddAlreadyMapped() {
        FizzBuzz.Builder<Integer> builder = new FizzBuzz.Builder<>(i -> i + 1, (i, j) -> i % j == 0);
        builder.add("Fizz", 3);
        builder.add("Buzz", 3);
    }

    @Test(expected = NullPointerException.class)
    public void testAddNullName() {
        FizzBuzz.Builder<Integer> builder = new FizzBuzz.Builder<>(i -> i + 1, (i, j) -> i % j == 0);
        builder.add(null, 3);
    }

    @Test(expected = NullPointerException.class)
    public void testAddNullDivisor() {
        FizzBuzz.Builder<Integer> builder = new FizzBuzz.Builder<>(i -> i + 1, (i, j) -> i % j == 0);
        builder.add("Fizz", null);
    }

    @Test
    public void testBuildWithoutAnything() {
        FizzBuzz.Builder<Integer> builder = new FizzBuzz.Builder<>(i -> i + 1, (i, j) -> i % j == 0);
        assertNotNull(builder.build());
    }

    @Test
    public void testBuildWithEverything() {
        FizzBuzz.Builder<Integer> builder = new FizzBuzz.Builder<>(i -> i + 1, (i, j) -> i % j == 0);
        builder.add("Fizz", 3);
        builder.add("Buzz", 5);
        builder.comparator(Comparator.naturalOrder());
        builder.joinDelimiter(" ");
        assertNotNull(builder.build());
    }
}

FizzBuzzTest

/**
 *
 * @author Frank van Heeswijk
 */
@RunWith(Enclosed.class)
public class FizzBuzzTest {
    private final FizzBuzz<Integer> fizzBuzz = FizzBuzz.<Integer>builder(i -> i + 1, (number, divisor) -> number % divisor == 0)
                    .add("Fizz", 3)
                    .add("Buzz", 5)
                    .build();

    @Test
    public void testBuilder() {
        assertNotNull(FizzBuzz.<Integer>builder(i -> i + 1, (i, j) -> i % j == 0));
    }

    @Test(expected = NullPointerException.class)
    public void testGetValueNullNumber() {
        fizzBuzz.getValue(null);
    }

    @Test(expected = NullPointerException.class)
    public void testRangeFromNullStart() {
        fizzBuzz.rangeFrom(null);
    }

    @RunWith(Parameterized.class)
    public static class Parametrized<T extends Number & Comparable<T>> {
        @Parameters
        public static Collection<Object[]> data() {
            Map<Integer, String> fizzBuzzIntegerResultMap = new HashMap<>();
            fizzBuzzIntegerResultMap.put(1, "1");
            fizzBuzzIntegerResultMap.put(2, "2");
            fizzBuzzIntegerResultMap.put(3, "Fizz");
            fizzBuzzIntegerResultMap.put(4, "4");
            fizzBuzzIntegerResultMap.put(5, "Buzz");
            fizzBuzzIntegerResultMap.put(6, "Fizz");
            fizzBuzzIntegerResultMap.put(7, "7");
            fizzBuzzIntegerResultMap.put(8, "8");
            fizzBuzzIntegerResultMap.put(9, "Fizz");
            fizzBuzzIntegerResultMap.put(10, "Buzz");
            fizzBuzzIntegerResultMap.put(11, "11");
            fizzBuzzIntegerResultMap.put(12, "Fizz");
            fizzBuzzIntegerResultMap.put(13, "13");
            fizzBuzzIntegerResultMap.put(14, "14");
            fizzBuzzIntegerResultMap.put(15, "FizzBuzz");

            Map<BigInteger, String> fizzBuzzBigIntegerResultMap = new HashMap<>();
            fizzBuzzBigIntegerResultMap.put(BigInteger.valueOf(1), "1");
            fizzBuzzBigIntegerResultMap.put(BigInteger.valueOf(2), "2");
            fizzBuzzBigIntegerResultMap.put(BigInteger.valueOf(3), "Fizz");
            fizzBuzzBigIntegerResultMap.put(BigInteger.valueOf(4), "4");
            fizzBuzzBigIntegerResultMap.put(BigInteger.valueOf(5), "Buzz");
            fizzBuzzBigIntegerResultMap.put(BigInteger.valueOf(6), "Fizz");
            fizzBuzzBigIntegerResultMap.put(BigInteger.valueOf(7), "7");
            fizzBuzzBigIntegerResultMap.put(BigInteger.valueOf(8), "8");
            fizzBuzzBigIntegerResultMap.put(BigInteger.valueOf(9), "Fizz");
            fizzBuzzBigIntegerResultMap.put(BigInteger.valueOf(10), "Buzz");
            fizzBuzzBigIntegerResultMap.put(BigInteger.valueOf(11), "11");
            fizzBuzzBigIntegerResultMap.put(BigInteger.valueOf(12), "Fizz");
            fizzBuzzBigIntegerResultMap.put(BigInteger.valueOf(13), "13");
            fizzBuzzBigIntegerResultMap.put(BigInteger.valueOf(14), "14");
            fizzBuzzBigIntegerResultMap.put(BigInteger.valueOf(15), "FizzBuzz");

            Map<Integer, String> fizzBuzzWoofIntegerReversedDivisorsResultMap = new HashMap<>();
            fizzBuzzWoofIntegerReversedDivisorsResultMap.put(1, "1");
            fizzBuzzWoofIntegerReversedDivisorsResultMap.put(2, "Woof");
            fizzBuzzWoofIntegerReversedDivisorsResultMap.put(3, "Fizz");
            fizzBuzzWoofIntegerReversedDivisorsResultMap.put(4, "Woof");
            fizzBuzzWoofIntegerReversedDivisorsResultMap.put(5, "Buzz");
            fizzBuzzWoofIntegerReversedDivisorsResultMap.put(6, "WoofFizz");
            fizzBuzzWoofIntegerReversedDivisorsResultMap.put(7, "7");
            fizzBuzzWoofIntegerReversedDivisorsResultMap.put(8, "Woof");
            fizzBuzzWoofIntegerReversedDivisorsResultMap.put(9, "Fizz");
            fizzBuzzWoofIntegerReversedDivisorsResultMap.put(10, "WoofBuzz");
            fizzBuzzWoofIntegerReversedDivisorsResultMap.put(11, "11");
            fizzBuzzWoofIntegerReversedDivisorsResultMap.put(12, "WoofFizz");
            fizzBuzzWoofIntegerReversedDivisorsResultMap.put(13, "13");
            fizzBuzzWoofIntegerReversedDivisorsResultMap.put(14, "Woof");
            fizzBuzzWoofIntegerReversedDivisorsResultMap.put(15, "FizzBuzz");

            Map<Integer, String> fizzBuzzIntegerWithReversedComparatorAndBlankStringJoinResultMap = new HashMap<>();
            fizzBuzzIntegerWithReversedComparatorAndBlankStringJoinResultMap.put(1, "1");
            fizzBuzzIntegerWithReversedComparatorAndBlankStringJoinResultMap.put(2, "2");
            fizzBuzzIntegerWithReversedComparatorAndBlankStringJoinResultMap.put(3, "Fizz");
            fizzBuzzIntegerWithReversedComparatorAndBlankStringJoinResultMap.put(4, "4");
            fizzBuzzIntegerWithReversedComparatorAndBlankStringJoinResultMap.put(5, "Buzz");
            fizzBuzzIntegerWithReversedComparatorAndBlankStringJoinResultMap.put(6, "Fizz");
            fizzBuzzIntegerWithReversedComparatorAndBlankStringJoinResultMap.put(7, "7");
            fizzBuzzIntegerWithReversedComparatorAndBlankStringJoinResultMap.put(8, "8");
            fizzBuzzIntegerWithReversedComparatorAndBlankStringJoinResultMap.put(9, "Fizz");
            fizzBuzzIntegerWithReversedComparatorAndBlankStringJoinResultMap.put(10, "Buzz");
            fizzBuzzIntegerWithReversedComparatorAndBlankStringJoinResultMap.put(11, "11");
            fizzBuzzIntegerWithReversedComparatorAndBlankStringJoinResultMap.put(12, "Fizz");
            fizzBuzzIntegerWithReversedComparatorAndBlankStringJoinResultMap.put(13, "13");
            fizzBuzzIntegerWithReversedComparatorAndBlankStringJoinResultMap.put(14, "14");
            fizzBuzzIntegerWithReversedComparatorAndBlankStringJoinResultMap.put(15, "Buzz Fizz");

            return Arrays.asList(
                new Object[] {
                    //regular FizzBuzz on Integer with {3, 5}
                    FizzBuzz.<Integer>builder(i -> i + 1, (number, divisor) -> number % divisor == 0)
                        .add("Fizz", 3)
                        .add("Buzz", 5)
                        .build(),
                    fizzBuzzIntegerResultMap,
                    1,
                    (Object)(UnaryOperator<Integer>)i -> i + 1
                },
                new Object[] {
                    //regular FizzBuzz on BigInteger with {3, 5}
                    FizzBuzz.<BigInteger>builder(bi -> bi.add(BigInteger.ONE), (number, divisor) -> number.mod(divisor).compareTo(BigInteger.ZERO) == 0)
                        .add("Fizz", BigInteger.valueOf(3))
                        .add("Buzz", BigInteger.valueOf(5))
                        .build(),
                    fizzBuzzBigIntegerResultMap,
                    BigInteger.valueOf(1),
                    (Object)(UnaryOperator<BigInteger>)bi -> bi.add(BigInteger.ONE)
                },
                new Object[] {
                    //FizzBuzz on Integer with {5, 3, 2}
                    FizzBuzz.<Integer>builder(i -> i + 1, (number, divisor) -> number % divisor == 0)
                        .add("Buzz", 5)
                        .add("Fizz", 3)
                        .add("Woof", 2)
                        .build(),
                    fizzBuzzWoofIntegerReversedDivisorsResultMap,
                    1,
                    (Object)(UnaryOperator<Integer>)i -> i + 1
                },
                new Object[] {
                    //regular FizzBuzz on Integer with {3, 5} with reversed comparator and blank string join
                    FizzBuzz.<Integer>builder(i -> i + 1, (number, divisor) -> number % divisor == 0)
                        .add("Fizz", 3)
                        .add("Buzz", 5)
                        .comparator(Comparator.reverseOrder())
                        .joinDelimiter(" ")
                        .build(),
                    fizzBuzzIntegerWithReversedComparatorAndBlankStringJoinResultMap,
                    1,
                    (Object)(UnaryOperator<Integer>)i -> i + 1
                }
            );
        }

        @Parameter(0)
        public FizzBuzz<T> fizzBuzz;

        @Parameter(1)
        public Map<T, String> resultMap;

        @Parameter(2)
        public T start;

        @Parameter(3)
        public UnaryOperator<T> increment;

        @Test
        public void testGetValue() {
            resultMap.forEach((number, fizzBuzzValue) -> assertEquals(fizzBuzzValue, fizzBuzz.getValue(number)));
        }

        @Test
        public void testRangeFrom() {
            String[] fizzBuzzValues = fizzBuzz.rangeFrom(start).limit(15).toArray(String[]::new);

            T element = start;
            for (String fizzBuzzValue : fizzBuzzValues) {
                assertEquals(resultMap.get(element), fizzBuzzValue);
                element = increment.apply(element);
            }
        }        
    }
}

I would like a review on every aspect of my code.

\$\endgroup\$
1
  • 1
    \$\begingroup\$ Can the downvoter please indicate what is wrong with this question? Other than that it involves FizzBuzz. \$\endgroup\$
    – skiwi
    Dec 23, 2014 at 19:51

1 Answer 1

10
\$\begingroup\$

Hm, how do I put this nicely: you have overengineered an exercise that's designed to serve as a basic sanity check to weed out talentless job candidates pretending as programmers. Aaaaaaaah! ;-) // kidding!

I understand your pain about fizz-buzz implementations that only work with int values; however, generics is not a great way to do this. The problem definition only makes sense with integers, which limits the possible Java types to:

  • int / Integer
  • long / Long
  • BigInteger

Since these types are too similar, this is not a typical pattern for using generics, in fact using generics for this is really awkward.

A better solution would be to use BigInteger internally, and provide overloaded methods for int and long, which will all convert their parameters to the largest supported type (BigInteger).

This will have the following advantages:

  • The fizz-buzz logic can remain encapsulated: no need to force users to pass in the divisible predicate. The duplicated logic in all unit tests and every time you use the class will go away.
  • The fizz-buzz logic can remain encapsulated: a fizzbuzzer is of not much use if you have to tell it how to fizzbuzz!
  • The code will become a lot simpler

Another thing, the rangeFrom doesn't belong to FizzBuzz class, as it adds an additional responsibility. It's a bit like feature creep. It seems convenient to provide this method, but it weakens the API. It would be better to move this logic to another class, for example FizzBuzzStream.fromRange(start, end), and stick to the single responsibility principle.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

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