4

As an example, let's assume our application needs some way to communicate with other systems using HTTP.

interface HttpClient {
  fun <T> get(url: String, returnType: Class<T>): T
  fun post(url: String, body: Any)
}

Now it seems like good practice to have our components depend on this interface and not on an implementation of the HttpClient - this makes it very easy for us to swap clients, either at author time or if you're being fancy even at runtime.

We decided to change our http client and really liked the way the Ktor client looked. The examples look great, using inline functions with reified type parameters definitely make the code much more readable. The public API Ktor (and, in fact, most other Kotlin implementations of http clients) provides looks something like this:

public suspend inline fun <reified T> get(url: String)

and calling these methods is nice

httpClient.get<MyResponse>("https://google.com")

compared to "the old fashioned java way"

httpClient.get("https://google.com", MyResponse::class.java)

However, this kind of API seems to make it impossible for me to simply swap HTTP clients, since inline functions cannot be used in interfaces, because the implementation used is chosen at runtime and inlining happens at compile time. In the examples of libraries like Ktor you see how easy it is to create these clients and use them, but in real projects you don't want every component to create & configure its own HTTP client itself, you would want to delegate this to some abstraction. Therefore, I argue, offering an inline only API forces you to couple classes that need to communicate through http to the implementation the library offers. This can be fine - I just think there should be a non-inline alternative offered as well. I could add another layer of abstraction over the http client, something like MyApi, but then I've pretty much lost all the convenience.

Wouldn't it make sense for Ktor (and many other libraries) to also offer an API like this to support a more decoupled design?

public suspend <T> fun get(url: String, Class<T>)

Secondly, I think these inline APIs make testing much harder. All of a sudden you don't have a separate component that you can potentially mock, you have code that will be inlined into the code that calls it. For example, this won't work anymore (example using the Mockk library)

every { httpClient.get(....) } returns SomeResponse()

Because of course, there is nothing left to mock - the get method doesn't exist anymore in the bytecode.

I haven't been able to solve this problem. You could argue that the httpClient shouldn't be mocked, but in most cases I don't want to go through the hassle of including the http client in my test - I have that tested separately and just want to test what my component does when the http client returns this response.

Ultimately my question is, do these kind of APIs in libraries hurt clean software design, or are there good ways to solve the problems I have described?

1
  • I don't have an answer to your question, but, having recently encountered a Kotlin web API client using inline and reification, I'm not a fan. Seems to overcomplicate things, and makes debugging harder because you can't dynamically evaluate any expressions involving the reified type.
    – Kyle McVay
    Nov 19, 2020 at 22:22

1 Answer 1

1

The main problem is that reification and inlining are currently tied together, which has some nice implementation properties, at the expense of some limitations in the interface. Kotlin hints in their documentation that this won't always be the case, but in the meantime, if you want the reification, you have to accept the inlining.

The function call is inlined, but you can still inject the object, which is how it appears Ktor is intended to be tested. I actually prefer these testkits provided by library authors to generic mocks. They tend to be better suited to the purpose and have fewer weird boundary conditions.

As far as being able to seamlessly swap out this implementation for a completely different library in the future, look at why you are swapping this one in right now. Are you able to use your old interface? No, because a large reason you are choosing this library is because of a cleaner interface. Another common reason to swap out a library is for new features, which again, won't be in the old interface. Another common reason is the old library going EOL, which is usually because people have moved on to other libraries with better interfaces.

It is exceedingly rare in practice to swap libraries with a drop-in replacement. You shouldn't spend a lot of effort now and sacrifice usability now on that very unlikely eventuality. Being able to swap in a test implementation is decoupled enough.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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