When it comes to naming your Java APIs (and their factory methods), choose your words carefully. Here, we'll cover how and when to use prepositions like of and with.
Join the DZone community and get the full member experience.Join For Free
Atomist automates your software deliver experience. It's how modern teams deliver modern software.
A friend of mine at Rutgers University would always respond to the question "What's up?" with the consistent response: "A preposition." I fell into this trap far too many times.
Have you ever thought about how much we use prepositions in our Java APIs?
We use several different prepositions in APIs in Eclipse Collections. Each one conveys a different meaning. Some prepositions that appear in Eclipse Collections multiple times are "with, of, by, as, to, from, into". When we use a preposition in an API, it should help convey meaning clearly. If it doesn't, then we would have been better off without it.
Two Prepositions Enter. One Preposition Leaves.
At JavaOne 2017, I described a battle we once had between two prepositions for naming our collection factory methods in the Eclipse Collections API. The battle was between of and with.
List<String> list = Lists.mutable.of("1", "2", "3"); // vs. List<String> list = Lists.mutable.with("1", "2", "3");
The prepositions of and with both work well for naming factory methods for creating collections. I personally prefer with, mostly because this is what was used with Smalltalk. In Smalltalk, I would regularly write the following:
|set| set := Set with: ‘1’ with: ‘2’ with: ‘3’.
The following is the equivalent using Java with Eclipse Collections.
Set<String> set = Sets.mutable.with("1", "2", "3");
If you prefer, you can also create a collection using the of factory method.
Set<String> set = Sets.mutable.of("1", "2", "3");
There are also forms that take an Iterable as a parameter. These are called ofAll and withAll.
In java.util.Collection, there are methods for adding and removing elements to and from collections. They are named add, addAll, remove, and removeAll. These four methods return boolean. This makes them unsuitable for writing code fluently.
We have our own Mutable interfaces in Eclipse Collections, so we knew we could fix the fluency problem by using one of the two prepositions. We selected with, because with has a natural opposite named without.
Set<String> set = Sets.mutable.with("1", "2", "3") .with("4") .without("2"); Assert.assertEquals(Sets.mutable.with("1", "3", "4"), set);
This naming pattern also worked well when adding elements via an Iterable.
Set<String> set = Sets.mutable.with("1", "2", "3") .withAll(Lists.mutable.with("4")) .withoutAll(Lists.mutable.with("1", "3")); Assert.assertEquals(Sets.mutable.with("2", "4"), set);
As you can see, with, withAll, without, and withoutAll are instance methods directly on our mutable collections. Instead of returning a boolean like add or remove, these methods return this, which is the collection that the method is operating on. These methods have good symmetry with the existing methods on Collection that return boolean, and also with each other.
We extended this pattern to our immutable collections as well.
ImmutableSet<String> set = Sets.immutable.with("1", "2", "3") .newWithAll(Lists.mutable.with("4")) .newWithoutAll(Lists.mutable.with("1", "3")); Assert.assertEquals(Sets.mutable.with("2", "4"), set);
In the mutable case, the withAll and withoutAll methods mutate the existing collection. In the newWithAll and newWithoutAll cases, a new collection is returned each time, thus preserving the immutability of the original collection.
Attack of the Clones
The preposition of lost the battle of the instance-based collection factory methods in Eclipse Collections because there is no good natural opposite for of like there is for with. That said, of is sometimes an important part of other method names in the Eclipse Collections API.
// Bag API - occurrencesOf MutableBag<String> bag = Bags.mutable.with("1", "2", "3"); Assert.assertEquals(1, bag.occurrencesOf("2")); // List API - indexOf MutableList<String> list = Lists.mutable.with("1", "2", "3"); Assert.assertEquals(1, list.indexOf("2")); // RichIterable API - sumOfInt, sumOfLong, sumOfFloat, sumOfDouble MutableList<String> list = Lists.mutable.with("1", "2", "3"); long sum = list.sumOfInt(Integer::parseInt); Assert.assertEquals(6L, sum); // RichIterable API - selectInstancesOf MutableList<String> list = Lists.mutable.with("1", "2", "3"); MutableList<String> filtered = list.selectInstancesOf(String.class); Assert.assertEquals(list, filtered);
Revenge of the With
With became more prevalent in the Eclipse Collections APIs when it was used to augment existing APIs like select, reject, collect, etc. The *With methods in the RichIterable API were originally added as optimizations. They allowed us to make anonymous inner classes static by providing more opportunities to make them completely stateless. As a completely independent and accidental benefit, the *With methods provide more opportunities for us to use Method References with Eclipse Collections APIs. This is a good thing because I have a Method Reference Preference. Here are some examples of using some of these methods with Method References using the domain from the Eclipse Collections Pet Kata.
boolean any = this.people.anySatisfyWith(Person::hasPet, PetType.CAT); Assert.assertTrue(any); boolean all = this.people.allSatisfyWith(Person::hasPet, PetType.CAT); Assert.assertFalse(all); boolean none = this.people.noneSatisfyWith(Person::hasPet, PetType.CAT); Assert.assertFalse(none); Person found = this.people.detectWith(Person::hasPet, PetType.CAT); Assert.assertNotNull(found); int count = this.people.countWith(Person::hasPet, PetType.CAT); Assert.assertEquals(2, count); MutableList<Person> selected = this.people.selectWith(Person::hasPet, PetType.CAT); MutableList<Person> rejected = this.people.rejectWith(Person::hasPet, PetType.CAT); PartitionMutableList<Person> partition = this.people.partitionWith(Person::hasPet, PetType.CAT); Assert.assertEquals(selected, partition.getSelected()); Assert.assertEquals(rejected, partition.getRejected());
Good API design is hard because naming is hard. It is a great feeling when you discover and use a name that communicates intent clearly to other developers. The best way to do that is to run your names by other developers you work with you to get a consensus before settling on a name.
On very rare occasions where a consensus is not possible (e.g. two equally good alternatives), either just pick a winner or take the cost of providing both. My preference is almost always to just pick a winner and move on. Providing both of and with factory methods will hopefully be a rare exception.
Published at DZone with permission of Donald Raab . See the original article here.
Opinions expressed by DZone contributors are their own.