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?