Well designed Fluent interfaces are great. The use of natural language like methods instead of properties and configuration classes make it possible to just jump in and start using complex APIs without the usual learning curve.
The problem is that fluent interfaces have become a fad and they are appearing everywhere even in APIs where there is no real place for them.
There are very many things I don’t like about the .Net SDK for Amazon Web Services (AWS), but the one thing that grates my nerves the most is the way the SDK uses fluent interfaces, but first one of the things I do like about the SDK: ‘It is consistent’.
All the operations of the SDK are accessed the same way:
- Create a client for the AWS service you need (or use an existing one).
- Create a request class specific to that operation with information about what you want to do.
- Call the method for that operation with the request class as a parameter and get a response.
So the code to upload a file to an S3 bucket (AWS’s file storage) looks like:
// Create the S3 service client
var s3Client = AWSClientFactory.CreateAmazonS3Client(accessKey, secretKey);
// Create the request object and configure it
var putRequest = new PutObjectRequest();
putRequest.WithBucketName(bucketName)
.WithKey(filename)
.WithCannedACL(S3CannedACL.PublicRead)
.WithContentType(mimeType))
.WithInputStream(fileStream);
// Get the (disposable) response
using (s3Client.PutObject(putRequest)){}
Note that the request object uses a fluent interface to set its properties and that the code itself is too verbose and does not look very fluent. In fact you’d expect to be able to rewrite the above code thus:
using (s3Client.PutObject(new PutObjectRequest()
.WithBucketName(bucketName)
.WithKey(blobId)
.WithCannedACL(S3CannedACL.PublicRead)
.WithContentType(mimeType)
.WithInputStream(fileStream)
)) {}
If so, you’d be wrong. The second more fluent snippet of code does not compile, but more on that later.
Fluid interfaces are a meta-language, a language built on top of the actual programming language and for a fluent interface to be good it should provide better semantics that the language it is replacing.
In order to upload a file (put an object) to S3 you must absolutely specify the name of the bucket you’re uploading to, the intended name for the file in S3 and the stream containing the data being uploaded. This means the WithBucketName, WithKey and WithInputStream methods of PutObjectRequest are not optional. Calling PutObject without first calling these methods results in an exception.
A more robust API design would have these properties as constructor parameters, so if a developer forgets to specify one of them the result will be a compile time error, not a run-time error that may or may not be caught during testing.
Strong typed languages are less prone to bugs caused by absent-mindedness than dynamic languages, because most typos and missing/misplaced parameters are caught at compile time. Dynamic languages trade robustness for ease and speed of development, which depending on the problem domain can be a good trade off; trading robustness for stylistic reasons, is not.
Using optional methods in a fluent interface to set properties should only be done if the properties have a sensible default value.
Fluid interfaces are a facade, the API equivalent of a user interface; and like good user interfaces they require a great deal of effort. The reason the second snippet of code does not compile is that the fluid interface of the .Net SD for AWS was not designed as a separate facade.
PutObjectRequest, like all the other request objects, inherits from S3Request. There is hopefully a valid architectural design reason for this, but it breaks the fluid interface:
WithBucketName is a method of PutObjectRequest that returns PutObjectRequest (as expected in a fluid interface,) but WithInputStream is a method of the base class and returns an S3Request that returns an S3Request.
S3Client.PutObject expects a PutObjectRequest as its only parameter.
The code
s3Client.PutObject(new PutObjectRequest().WithInputStream(fileStream))
won’t compile because the type S3Client returned by WithInputStream is not assignable to the input argument of type PutObjectRequest expected by the method.
For a similar reason:
var p = new PutObjectRequest().WithBucketName(bucketName).WithInputStream(fileStream);
compiles, but:
var p =new PutObjectRequest().WithInputStream(fileStream).WithBucketName(bucketName);
does not.
In the end one has to wonder whether a fluid interface makes any sense in this case.
A non-fluid version of the call to PutObject could consist of:
using (s3Client.PutObject(new PutObjectRequest(bucketName,
blobId,
fileStream)
{
CannedAcl = S3CannedACL.PublicRead,
ContentType = mimeType
}) {}
Is there really any benefit in converting this to a fluid interface?