A common anti-pattern often found in connectors is that the ConnectionProvider generates a connection object that exposes the client or implementation used to access the external system. For example:
public class HttpConnection {
private HttpClient client;
public HttpClient getClient() {
return client;
}
}
An operation would then use it like in this pseudo code:
public void createCustomer(@Connection HttpConnection connection, @Content InputStream content) {
connection.getClient().send(HttpRequest.builder()
.path(connection.getPath() + "/customer")
.method("POST")
.entity(content)
.addHeader("Accept", "application/json")
.build()
);
}
This code has several drawbacks. The most obvious is that the operations using the connection are strongly coupled to the implementation of the connection object. If the need ever comes to change the implementation of the client, all the components using that connection will be affected.
Another problem is that the connection object is not testable. Because the client object is exposed, writing a test that interacts with a mock version of the connection object becomes too complicated.
The biggest problem is that it introduces functional coupling between all components using the connection object and the functional nuances of all supported connection providers.
For example, assume that the example above is part of a connector that supports both Basic Authentication and OAuth authorization mechanisms.
If invalid credentials are used with the Basic Authentication connection, then the request will result in a HTTP 401 status code and the operation should fail.
However, if the same response code is received using an OAuth protected connection, then the connector needs to execute logic to determine if the token has expired and an AccessTokenExpiredException should be thrown.
As new connection types and components are added, the worse the problem becomes. This is only one example. It can happen with all other types of connections, not just HTTP clients.
To prevent this, the connection objects must encapsulate their inner communication mechanisms and security schemes, leading to a pattern like this:
public void createCustomer(@Connection HttpConnection connection, @Content InputStream content) {
connection.createCustomer(content);
}
With this approach, each ConnectionProvider implementation can provide its own implementation of the HttpConnection object, removing the root cause of the problem and providing freedom to change the inner workings of the connection without affecting other components.
Alternative: Leverage the Command Pattern
Instead of giving the connection object one method per endpoint to be consumed, another option is to implement the command design pattern in a more generic way:
public void createCustomer(@Connection HttpConnection connection, @Content InputStream content) {
connection.request(HttpRequest.builder()
.path(connection.getPath() + "/customer")
.method("POST")
.entity(content)
);
}
With this approach, the connection object has only one generic request method which receives an HttpRequestBuilder object. Notice that the builder object never receives the build() command. Depending on the implementation of the connection object, additional headers can be added, the request can be performed in different ways and the response can be processed accordingly.