A couple of lessons I learned about API design

by Алекс Руис on July 13, 2009

One of the reasons I work on open source is to become a better programmer. Learning opportunities come from different sources, being practice the more obvious one. In the last couple of weeks I learned two important lessons about API design not by practicing my craft, but from listening to our users.

What I learned may appear as common sense by most of you. But I’d like to share anyway. I found out that common sense is not always common :)

Lesson #1: Do not sacrifice flexibility and extensibility by overprotecting your users

It started with this thread, in which Szczepan Faber, creator of Mockito, mentions that he recently found FEST’s Assertions module and it “seems very interesting.” He suggested that, in order to make the library more extensible, the class Assertions and the classes implementing assertion methods could be made non-final.

I admit I’ve been adamant to open up those classes. I rejected previous requests due to the following reasons:

  1. The class Assertions contains only static methods: a bunch of overloaded assertThat methods. I thought it is a “best practice” to access static methods from the declaring class only (the Eclipse compiler even has a setting to enforce this.) I honestly didn’t see the point of subclassing Assertions. I thought that if somebody needed to extend it, he or she could use composition instead.
  2. Classes containing actual assertion methods are also final (e.g. StringAssert) due to the lack of self-types in Java, the language. I’ll explain with an example. The assertion methods in StringAssert always return this, to facilitate method chaining
    public StringAssert isNotNull() {
      assertNotNull();
      return this;
    }

    so we can write something like

    assertThat(someString).isNotNull()
                          .isEqualTo("Hello World");

    If we need to subclass StringAssert, we need to override its methods to return the subtype, otherwise, when chaining methods, we will not see the new methods in the new subclass

    @Override public MyStringAssert isNotNull() {
      super.isNotNull();
      return this;
    }

    Once again, I thought that by using composition users may have better chances of not falling into this trap.

Szczepan responded:

Hmmmm, I see point now. I would still recommend to un-finalize the assert classes… Internally, you can probably figure out some way of verifying if you didn’t forget to override (like extensive functional test suite, PMD rule or JUnit test that reflects the methods, etc.) Externally, I’d like to have an easy way to extend assertions on already handled types. Extension via custom conditions doesn’t work for me because it’s too Hamcresty and I’m doing FEST here, right? :)

Szczepan provides a use case for FEST’s API I never thought about before. Not only that, he also provides a pragmatic solution to the “problem” I was afraid of!

Soon after reading his response I realized I’ve been over-worrying about users not following certain “good practice” or forgetting to return the correct type. The worst-case scenarios are not as bad as I thought, and users of FEST’s API can easily recover from a mistake. I realized I’ve been limiting extensibility and flexibility of the API by over-protecting users.

Lesson #2: Too many options may cause confusion

In the same thread, Szczepan suggests to add the methods is and has as “humanized” aliases for satisfies, to make the API more compact and readable. The following example is taken from FEST’s wiki:

assertThat("hello").as("Greeting").satisfies(isUppercase());
// using the "is" alias:
assertThat("hello").as("Greeting").is(uppercase());

Although it makes a lot of sense (and I actually filed tasks to have this aliases in our next release,) Ansgar Konermann, another FEST user, brings up a very interesting point:

I like FEST assertions for its simple API. I think it is a significant plus to attract new users. We should take care not to complicate the API unnecessarily. When I was new to the FEST API, I even considered it strange to have both as() and describedAs(), which do exactly the same thing. I later understood this is a technical necessity to allow usage of FEST in Groovy. I’d love to see FEST keep its API as simple as possible.

Think about http://c2.com/xp/OnceAndOnlyOnce.html

I’d agree that satisfies/doesNotSatisfy sounds like math, not like a check of domain conditions. Nevertheless, I feel that this basic concept of a condition/predicate is absolutely valid to use when defining tests. We also kind of agree that assertThat is fine for all tests, however your business analyst would probably express this a bit different (e.g. makeSureThat, itMustAlwaysBeTheCase). If someone asked me, I’d opt not to include each and every variant which a natural language might have developed for the same meaning over time, but to convey this meaning using exactly one, carefully chosen word.

Ansgar has a valid point. Aliases can add more flexibility to an API, but it also may increase confusion to its users. Finally he adds:

In our team, the expectation of FEST’s API is: type assertThat(actual), press CTRL+Space and instantly _know_ which single method to call for the check you want to perform. No further thinking about which method to choose. Given a test intention, it should be totally obvious which method to call. Aliasing makes this harder. Don’t make me think – at least not about the methods which I need to call. When writing tests, I instead want to focus my thinking on the conditions which should be checked.

Pretty clear message. Nothing else to add.

In Conclusion

What I like the most about these two lessons I learned is that they seem to pull in opposite directions. Finding the balance between these two may help create an API that is both flexible, extensible and at the same time, does not offer too many choices that may cause confusion.

As I mentioned earlier, working on open source is a great experience. It helps us stay humble, since we are always going to find somebody with better ideas than ours.

Thanks Szczepan and Ansgar!

Update: This blog entry has been added to JavaLobby. Many thanks to the JavaLobby folks!

(Image taken from BL1961′s flickr stream under the creative commons license)

{ 11 comments… read them below or add one }

Szczepan Faber July 13, 2009 at 8:49 am

Very nice.

Too many aliases are confusing and may decrease usability. Still, I love them. I read more code than I write.

Reply

Alex Ruiz July 13, 2009 at 3:56 pm

I agree, I like to stare at readable code as if it was a paint or some other piece of art :)

Reply

pramatr July 14, 2009 at 1:18 am

Great post! I absolutely agree with the comments and message; “I read more code than I write” and “it helps us stay humble”. Far too often you speak to developers who already know it, they’ve done it before they don’t need to learn a new way. It’s so nice and refreshing when people say, “you know what you’re right maybe we do need to look at this” even though it flys in the face of truths we held before. It’s about balance as you’ve said, but you’ve got to listen to your users because if you don’t someone else will.

Reply

Tom July 14, 2009 at 5:19 am

Good post.
Thanks for sharing your thinking and and the ones of others.

I heard that the Ruby language has a lot of aliases and it tends to confuse new users. Ok, with Ruby, there is no real good JavaDoc…

Reply

David July 14, 2009 at 7:02 am

A solution to your lesson #1 problem would be to use generics for the base assertions class:

class StringAssert {
    ...
    public T isNotNull() {
        assertNotNull();
        return this;
    }
    ...
}

Then any subclass can cite itself in its definition:

class MyStringAssert extends StringAssert {
    ...
}

This would avoid necessitating overrides for every method for a sub-class. In addition, the generics api access will be hidden from the end-user (unless they decide to sub-class) via the static accessors which avoids any changes to the standard use case. If a user forgets to specify the generic type when sub-classing, compilers/ides will issue a warning which can act as a reminder to do so. Major downside is a minimum requirement for jdk5.

Reply

David July 14, 2009 at 7:05 am

@David

Alright, forgot html-ified comments… re-posted code for above:

class StringAssert <T> {
…
public T isNotNull() {
  assertNotNull();
  return this;
}
…
}

class MyStringAssert extends StringAssert<MyStringAssert> {
…
}

Reply

Alex Ruiz July 14, 2009 at 9:00 am

@David

Hello David,

The problem with your solution is that we cannot use StringAssert as we do now.

Regards,
-Alex

Reply

paulo July 14, 2009 at 10:07 am

Lately i’ve been using Enums with a interface so that i can have self-evident polymorphism. Just CTRL+SPACE and pass that enum into a method requiring a interface, and still be able to extend externally.

Reply

Alex Ruiz July 14, 2009 at 10:21 am

@paulo

Hello Paulo,

I’d like to see an example :)

Thanks,
-Alex

Reply

dev dude July 14, 2009 at 5:03 pm

You could even apply the once and only once to the Java bean naming conventions for setters/getters. I can’t tell you how many times I have started with typing in ‘get’ for a property and then tried the code completion shortcut key, only to find the property getter isn’t listed because it is a boolean and the method name starts with ‘is’ instead of ‘get’. Both are legal, but the ‘is’ means we have to do some extra thinking in order to pay the price for code that is a little more readable. I am not sure it is worth it. This is even more true for third party APIs. I try to keep my interfaces consistent with each other, with little clues in the method names, but sticking to consistent names rather than going off the deep end with a lot of verbage. I’ve dropped the use of aliases (for the most part) a long time ago, and even many convenience wrappers, and I try to keep the interface trimmed down as much as possible and still give the maximum functionality – it is a balance.

I rarely use final classes – I am still a strong believer in extensibility, and I have seen more and worse abuse/misuse of composition than I have inheritance – although I have seen inheritance lead to a mess too. Again, it is a balance.

Reply

reboltutorial July 19, 2009 at 3:20 pm

I don’t agree as for aliases: it can be usefull to clarify the intent depending on the context one can choose one or the other, but it would be better if one can customize aliases like stereotypes in UML.

Reply

Leave a Comment

{ 2 trackbacks }

Previous post:

Next post: