By Sourav Mehta
Writing a simple API is already an art on its own, but keeping it simple for beginners while also making it extensible for power users presents an even greater challenge. In this article, we will explore the concept of "extensible" APIs and discuss different approaches to achieve this. We will use jOOQ, a popular Java library for SQL querying, as an example.
Before diving into the specifics, let's first understand what "extensible" means in the context of APIs. Essentially, an extensible API is one that allows users to customize or extend its functionality beyond the default behavior. It gives power users the flexibility to tailor the API to their specific needs without sacrificing simplicity for beginners.
jOOQ is known for its clean and simple API, which allows users to write SQL predicates with ease. For example:
ctx.select(T.A, T.B)
.from(T)
.where(T.C.eq(1))
.fetch();
By default, jOOQ generates and executes the corresponding SQL statement using a bind variable. This approach makes the most common use case simple and intuitive.
However, power users may occasionally require more flexibility, such as the ability to not use bind variables. jOOQ offers two ways to address this:
On a per-query basis: Users can turn a variable into an inline value explicitly for a single occasion using the inline() method:
ctx.select(T.A, T.B)
.from(T)
.where(T.C.eq(inline(1)))
.fetch();
While this approach works, it may become cumbersome if users need to do this for multiple queries or bind values.
On a global basis: jOOQ provides a DSLContext object that represents the contextual DSL within a jOOQ Configuration. Users can set this object to enable global changes to the API's behavior:
ctx2 = DSL.using(ctx
.configuration()
.derive()
.set(new Settings().withStatementType(StatementType.STATIC_STATEMENT));
ctx2.select(T.A, T.B)
.from(T)
.where(T.C.eq(1))
.fetch();
This approach allows users to make changes to the API globally, affecting all subsequent queries. It provides more convenience but may not be suitable for every use case.
While the above approaches serve their purpose, there are better alternatives to offering extensibility in APIs. Let's explore two popular methods:
Dependency Injection (DI): With DI, users rely on a container like Spring to inject objects into their method calls. This approach allows users to access contextual information when needed. For example, in a JAX-RS HTTP API:
@GET
@Produces("text/plain")
@Path("/api")
public String method(@Context HttpServletRequest request, @QueryParam("arg") String arg) {
...
}
DI works well in static environments where dynamic URLs or endpoints are not an issue. It simplifies the process of accessing framework objects but may require a learning curve for users unfamiliar with the DI framework.
Service Provider Interfaces (SPIs): jOOQ adopts an SPI approach to offer extensibility. SPIs provide a central place for users to register implementations that can be used for various purposes. jOOQ's Configuration serves as this central access point. Users can register SPI implementations to modify the API's behavior selectively.
For example, jOOQ's Configuration allows users to set a JSR-310 java.time.Clock object, which affects timestamp generation:
Configuration configuration = new Configuration();
configuration.setClock(new MyCustomClock());
By providing a single point of access for customization, jOOQ's API remains simple by default. Users have the flexibility to extend the API according to their needs, without compromising its simplicity.
Writing a simple yet extensible API is undoubtedly a challenge. However, by understanding the concept of extensibility and adopting suitable approaches like Dependency Injection and Service Provider Interfaces, API designers can strike a balance between simplicity and flexibility. jOOQ's design choices, such as its Configuration-based SPI approach, serve as a great example for API vendors to offer extensibility without sacrificing simplicity.