ProAndroidDev

The latest posts from Android Professionals and Google Developer Experts.

Follow publication

Keep your interfaces simple

Danny Preussler
ProAndroidDev
Published in
3 min readJul 16, 2020

https://unsplash.com/photos/xxeAftHHq6E

Writing your classes with a good API is hard but important. As the writer is trying to make it easy for the user, we sometimes tend to repeat ourselves by adding convenient methods.

Think about the List interface in Java. To check if there are no elements in the list we could check list.getLength() == 0 or we simply ask for isEmpty().

The 2nd one reads much better. But it also adds a duplication and implicit connection between the two: If the list is empty, it can’t contain any elements! This must be respected by every implementer of the interface!

We can easily think of many other methods with implicit dependencies. Think about how hashCode and equals have a connection. This is stated in the Javadocs:

Note that it is generally necessary to override the hashCode method whenever equals is overridden, so as to maintain the general contract for the hashCode method, which states that equal objects must have equal hash codes.

This is a pitfall that can be difficult to avoid and can lead to issues elsewhere that are difficult to track down and fix. Nowadays we have tools to validate this contract, or even better, to generate the implementation.

Another example: feature flags

A lot of developers work with features flags. These enable us to release continuously without the need for long-lived feature branches. Let’s say we have an interface like this:

interface AppFeatures {
fun isEnabled(feature: Feature): Boolean
}

When using this I realized I often write code like:

if (!isEnabled(Feature.SomeFeature))

“not is enabled” does not read nicely though. But I want the reader to understand my code without thinking too much. Therefore my initial thought was to add another method to the interface

fun isDisabled(feature: Feature): Boolean

But I realized, doing this might break a lot of tests that simply mock the interface:

val appFeatures = mock<AppFeatures> {
on
{ isEnabled(Features.SomeFeature) } doReturn true
}

If I were to change the implementation to use isDisabled, the test would fail. But all I changed was an implementation detail without any actual changes. The problem with this is that, again, both methods have a hidden contract: A feature is either disabled or enabled, never both. Every implementation needs to comply, and that includes mocks, but mocks are generated implementation! Our mocking frameworks don’t know about hidden contracts we might have written (probably hidden in some JavaDoc if the contract isn’t so obvious).

This example also shows us why we should try to avoid mocking and instead use a stub implementation.

Let’s take a step back and ask ourselves if it is wise to add this method in the first place. It’s a convenient solution, but also dangerous as we can’t express the contract between two methods in the tools we have today.

Fortunately, Kotlin has some features that could help us.

Partial solution: Default implementations

We could add the method including its implementation directly to the interface:

fun isDisabled(feature: Feature) = isEnabled(feature).not()

This way the contract is fixed. The implementer just need to add isEnabled

Unfortunately, this will not work in mocking libraries like Mockito Kotlin. The reason is that in bytecode there is no actual implementation, it is still a normal Java-like interface. This means your mock is not gonna have the code of isDisabled and instead will return the default valuefalse and therefore break the contract.

Extension functions to the rescue

But we have another tool at our disposal. Instead of polluting our interfaces, we can move these comfort functions and still retain their usefulness. Kotlin’s extension functions are exactly about that:

fun AppFeatures.isDisabled(feature: Feature) = 
isEnabled(feature).not()

This has many advantages:

  • It doesn’t need to be implemented by fakes or stubbed out in mocks
  • The interface stays simple and clean
  • The contract is fixed
  • Works with the way we want to use mocks in tests

Extension functions are a very powerful tool. Many developers don’t use them to their full capabilities. So next time you define an API, remember the lesson above.

Edit: Aidan Mcwilliams looked into how you could even use more Kotlin features on this, continue your read here.

Published in ProAndroidDev

The latest posts from Android Professionals and Google Developer Experts.

Written by Danny Preussler

Android @ Soundcloud, Google Developer Expert, Goth, Geek, writing about the daily crazy things in developer life with #Android and #Kotlin

Responses (1)

Write a response