Saturday, March 1, 2014

Builder pattern in Java

I've always liked how nice the StringBuilder API is. But I have not found the perfect opportunity to use it until today when I wanted to create a clean API for a Request object for searching OnDemand videos. The server API has been defined to accept a bunch of parameters. And depending on what you want to search for, you may supply one or more of these parameters.

What I wanted to create was a request object that's immutable once it's created. I have plans to use this request as a key to retrieve cached response, so it would be messy if someone was allowed to modify the attributes of the original request. So I've decided to have no setter methods.

I could create a constructor with all the parameters, but that would mean that most of the time the developer may have to create a SearchRequest object with lots of nulls. It's also very easy to misplace the arguments. If argument 1 is the rating and argument 2 is the offset, and they're both integers, I can imagine someone accidentally putting the value for offset in the ratings position and the value for ratings in the offset position.

The Builder pattern can be helpful in my situation. So I've prototyped the class here. Let's take a look at how I use this class first. Here's an example of a SearchRequest that query for high rating sports TV shows and episodes.

SearchRequest request = new SearchRequest.SearchRequestBuilder()
.ofType(SearchRequest.MediaType.TV_SERIES)
.ofType(SearchRequest.MediaType.TV_EPISODE)
.inCategory(SearchRequest.MediaCategory.SPORTS)
.withRatingsHigherThan(4)
.toSearchRequest();
System.out.println(request.getURL());
view raw gistfile1.java hosted with ❤ by GitHub

Running this code generates the following print:
03-01 20:00:59.849    1321-1321/com.example.app I/System.out﹕ http://my.server.com/search.json?offset=0&max_result=100&rating=4&categories=SPORTS&types=TV_EPISODE,TV_SERIES

I've restricted the creation of the SearchRequest object directly by making the constructor private. Instead the developer need to create a new SearchRequestBuilder. The builder then has small methods that are clearly named to add attributes. Once you have composed the request you want, you call the toSearchRequest method and you get the SearchRequest object.

And here's the source code for SearchRequest. I've seen variations of this pattern, but I've implemented it to satisfy my own requirements. I'd love to hear how you've used this pattern in the course of your career and how it differs from my interpretation.

package com.example.app;
import java.util.HashSet;
import java.util.Set;
/**
* SearchRequest is an object to help create a valid REST URL for searching TV Shows and movies...
*/
public class SearchRequest {
public static enum MediaType {
VIDEO_CLIP,
TV_SERIES,
TV_EPISODE,
MOVIE
}
public static enum MediaCategory {
NEWS,
SPORTS,
KIDS
}
public static class SearchRequestBuilder {
private SearchRequest request;
public SearchRequestBuilder() {
request = new SearchRequest();
}
public SearchRequestBuilder ofType(SearchRequest.MediaType mediaType) {
request.mTypes.add(mediaType);
return this;
}
public SearchRequestBuilder inRange(int offset, int maxResult) {
request.mOffset = offset;
request.mMaxResult = maxResult;
return this;
}
public SearchRequestBuilder matchingTitle(String title) {
request.mTitleQuery = title;
return this;
}
public SearchRequestBuilder inCategory(SearchRequest.MediaCategory categories) {
request.mCategory.add(categories);
return this;
}
public SearchRequestBuilder withRatingsHigherThan(int rating) {
request.mRating = rating;
return this;
}
public SearchRequest toSearchRequest() {
return request;
}
}
private Set<MediaType> mTypes = new HashSet<MediaType>();
private Set<MediaCategory> mCategory = new HashSet<MediaCategory>();
private String mTitleQuery;
private int mRating = 0;
private int mOffset = 0;
private int mMaxResult = 100;
//constructors only accessible by the Builder
private SearchRequest() {}
public String getURL() {
StringBuilder builder = new StringBuilder();
builder.append("http://my.server.com/search.json?");
builder.append("offset=").append(mOffset);
builder.append("&max_result=").append(mMaxResult);
builder.append("&rating=").append(mRating);
if (mTitleQuery != null) builder.append("&titleQ=").append(this.mTitleQuery);
if (mCategory.size() > 0) builder.append("&categories=").append(toQueryValue(mCategory));
if (mTypes.size() > 0) builder.append("&types=").append(toQueryValue(mTypes));
return builder.toString();
}
private String toQueryValue(Set<?> items) {
StringBuilder builder = new StringBuilder();
for (Object item : items) {
if (builder.length() > 0) builder.append(",");
builder.append(item.toString());
}
return builder.toString();
}
}

No comments:

Post a Comment