Tidying up the loose ends
Some of you might have been a little incredulous at my earlier statement that no existing library offers methods to neatly deal with possibly-null varargs.
I stand by my position - it's the possibly-null aspect that is the kicker. Google Guava offers methods that almost get over the line:
My varargs method | Google's almost-the-same method |
---|---|
Iterable<T> VarargIterator.forArgs (T... optionalArgs) |
ArrayList<E> Lists.newArrayList (E... elements) |
Iterable<T> VarargIterator.forCompulsoryAndOptionalArgs(T t, T... ts) | List<E> Lists.asList (E first, E[] rest) |
BUT they explode when fed nulls - making them pointless for this particular wart-removing exercise!
So the only thing left now is to clean up the implementation a little, because I think it's failing two of the rules of clean code:
- Runs all the tests
- Contains no duplications
- Expresses the intent of the programmers
- Minimizes the number of classes and methods
Here's the entire class as it stands (imports removed):
public class VarargIterator { /** * @return an {@link Iterable} of a suitable type. * Null-safe; passing a null varargs will simply return * an "empty" {@code Iterable}. */ public static final <T> Iterable<T> forArgs(T... optionalArgs) { if (optionalArgs == null) { return Collections.emptyList(); } else { return Arrays.asList(optionalArgs); } } /** * @return an {@link Iterable} of a suitable type. * Null-safe; passing a null {@code optionalArgs} will simply return * a single-element {@code Iterable}. * @throws IllegalArgumentException if {@code compulsoryArg} is null */ public static final <T> Iterable<T> forCompulsoryAndOptionalArgs( final T compulsoryArg, final T... optionalArgs) { Validate.notNull(compulsoryArg, "the first argument to this method is compulsory"); if (optionalArgs == null) { return Collections.singletonList(compulsoryArg); } else { List<T> iterable = new ArrayList<T>(optionalArgs.length + 1); iterable.add(compulsoryArg); iterable.addAll(Arrays.asList(>optionalArgs)); return iterable; } } }
The bits that I don't like are the repetition of the optionalArgs null check in both methods, and the code in the else case of the second method. It's working at a "different level" to the other code in the method, losing the intent, and there's too much of it - it's made the whole method too long.
Of course I have a full suite of unit tests so I can be confident that I'm not breaking anything when I do this refactoring work. I use Cobertura in its Maven and Eclipse plugin forms to ensure I'm achieving 100% coverage from these tests.
The first step is easy - extract out a well-named method for the else case:
/** * @return an {@link Iterable} of a suitable type. * Null-safe; passing a null {@code optionalArgs} will simply return * a single-element {@code Iterable}. * @throws IllegalArgumentException if {@code compulsoryArg} is null */ public static final <T> Iterable<T> forCompulsoryAndOptionalArgs( T compulsoryArg, T... optionalArgs) { Validate.notNull(compulsoryArg, "the first argument to this method is compulsory"); if (optionalArgs == null) { return Collections.singletonList(compulsoryArg); } else { return createListFrom(compulsoryArg, optionalArgs); } } private static <T> Iterable<T> createListFrom( T compulsoryArg, T... optionalArgs) { final List<T> list = new ArrayList<T>(optionalArgs.length + 1); list.add(compulsoryArg); list.addAll(Arrays.asList(optionalArgs)); return list; }
I dithered a bit about the null check. Doing some kind of is-null/is-not-null closurey-interfacey thing seemed like pretty major overkill, so in the end I just extracted out the logic into a method. As a bonus, I realised I could also check for a zero-length (as opposed to null) array and save some cycles in that case. One last tweak was to drop the explicit else branches - because the methods are now so short there seems little point. So here's the final version - enjoy!
public class VarargIterator { /** * @return an {@link Iterable} of a suitable type. * Null-safe; passing a null varargs will simply return * an "empty" {@code Iterable}. */ public static final <T> Iterable<T> forArgs(T... optionalArgs) { if (isEmpty(optionalArgs)) { return Collections.emptyList(); } return Arrays.asList(optionalArgs); } /** * @return an {@link Iterable} of a suitable type. * Null-safe; passing a null {@code optionalArgs} will simply return * a single-element {@code Iterable}. * @throws IllegalArgumentException if {@code compulsoryArg} is null */ public static final <T> Iterable<T> forCompulsoryAndOptionalArgs( T compulsoryArg, T... optionalArgs) { Validate.notNull(compulsoryArg, "the first argument to this method is compulsory"); if (isEmpty(optionalArgs)) { return Collections.singletonList(compulsoryArg); } return createListFrom(compulsoryArg, optionalArgs); } private static boolean isEmpty(Object... optionalArgs) { return (optionalArgs == null) || (optionalArgs.length == 0); } private static <T> Iterable<T> createListFrom( T compulsoryArg, T... optionalArgs) { final List<T> list = new ArrayList<T>(optionalArgs.length + 1); list.add(compulsoryArg); list.addAll(Arrays.asList(optionalArgs)); return list; } }
No comments:
Post a Comment
Comments welcome - spam is not. Spam will be detected, deleted and the source IP blocked.