Template pattern with real-world example
In this tutorial, we will solve the a real-world problem and solve it with the template pattern.
The real-world business problem that I had in my real job
We had one microservice that just worked as a proxy to Salesforce.
Our proxy service had multiple endpoints such as getCarsByCriteria
, getPersonsByCriteria
, getCarsWithExpiredInsurance
, etc. Inside the proxy service, we transform the API request into the Salesforce queries(similar to the SQL queries), call Salesforce through HTTP, and convert that result to the proxy service response. So simple.
When I joined the company, that service was already written and it was in terrible shape. The code inside the proxy service looked like this:
public class CarService {
private SalesForceClient salesForceClient;
public List<CarResponse> getCarsWithExpiredInsurance() {
salesForceClient.get("select id,car_name__c,car_brand from car where expired = true");
//mapping to the reponseList
return reponseList;
}
public List<CarResponse> getAllCars() {
salesForceClient.get("select id,car_name__c,car_brand from car");
//mapping to the reponseList
return reponseList;
}
public List<CarResponse> getAllCarsWithCertainBrand(String brand) {
salesForceClient.get("select id,car_name__c,car_brand from car where brand = '" + brand + "'");
//mapping to the reponseList
return reponseList;
}
}
Problems of this approach
As you can see, pure and simple string concatenation. Problems are:
- With every new query, there was no reusing of old code. When you want to put strings inside
where
part, strings are surrounded by'
but when you put dates, they should not be surrounded by'
. There are a bunch of other small things that you cannot reuse from query to query. - There is no structure at all. When adding a new query that needs to fetch a person and its cars, developers were uncertain if they should add that feature to
CarService
,PersonService
, or maybe create a new service. - If you want to add pagination to all queries, you need to append that in all queries or somehow wrap all services with some magic to support pagination.
- There is no way to unit test only query creation because
CarService
breaks the Single Responsibility principle.CarService
creates a query and then calls the Salesforce client. Those two things need to be separated.
Resolve problem step by step
Firstly, I helped one of my colleagues apply builder and composite patterns to this string concatenation. You can read more about composite here and about builder here. The result of applying those patterns is that now we can create queries in the following fashion:
QueryBuilder queryBuilder = QueryBuilder.builder()
.select(Arrays.asList("id", "name", "age"))
.from("persons")
.where(
new NotEqualsCondition("id", 5)
.and(new EqualsCondition("name","Rick"))
.and(new LikeCondition("address", "Street%"))
.and(new HigherEqualsCondition("age",18))
.and(new EqualsCondition("hairColor", "Black"))
.or(new InCondition("city",String.join(",", Arrays.asList("Manchester", "London"))))
.or(new InCondition("city",String.join(",", Arrays.asList("Paris", "Nice"))).not())
).groupBy("country").orderBy("creationDate").limit(10).build();
QueryBuilder
class is a result of the builder pattern and all of those *Condition
classes are consequences of the composite pattern.
This looks much better than string concatenation. But still, we have a problem that for every new query, we need to create a new method, put that method in some service, there is still no structure between queries, etc.
Now, let’s think. Every query has a similar template. It must contain select
and from
parts and optional parts are where
, group by
, and order by
. Every query should override those parts specifically.
Template pattern applied
Let’s create that template first:
public abstract class QueryCreator {
public abstract List<String> select();
public abstract String from();
public abstract <T> Condition where(T request);
public String groupBy() {
return null;
}
public <T> String createQuery(T request) {
QueryBuilder.Builder builder = QueryBuilder
.builder()
.select(select())
.from(from())
.where(where(request))
.groupBy(groupBy());
String soql = builder.build().getQuery();
return soql;
}
}
and let’s implement that template for one concrete query(getAllCarsWithCertainBrand
for example)
public class GetAllCarsWithCertainBrand extends QueryCreator {
@Override
public List<String> select() {
return asList("id", "car_name__c","car_brand");
}
@Override
public String from() {
return "car";
}
@Override
public <T> Condition where(T request) {
GetAllCarsWithCertainBrandRequest req = (GetAllCarsWithCertainBrandRequest) request;
return new EqualsCondition("brand",req.getBrand());
}
}
This is how we use our creator:
public class CarService {
private GetAllCarsWithCertainBrand getAllCarsWithCertainBrand;
public List<CarResponse> getAllCarsWithCertainBrand(GetAllCarsWithCertainBrandRequest request) {
salesForceClient.get(getAllCarsWithCertainBrand.createQuery(request));
//mapping to the reponseList
return reponseList;
}
}
Important to notice is that abstract template logic is inside the QueryCreator
class and the specific implementation of the template is in concrete classes that extend QueryCreator
(in our example we have one query GetAllCarsWithCertainBrand
). As you can see, the responsibility to call salesforce is now inside CarService
. Responsibility to create a query is inside GetAllCarsWithCertainBrand
which means that we satisfy the Single responsibility principle.
Conclusion
All listed problems are solved with this approach:
- You implemented salesforce syntax inside your builder so you don’t need to implement that again and again.
- There is a simple structure(our template). When you want to support a new query, create a new subclass of
QueryCreator
and that’s it. - Since now the responsibility of
QueryCreator
is to create a query, you can create a unit test for it. - If you want to support pagination for all queries, you can do that inside
createQuery
method. You can also expose oneorderBy
method fromQueryCreator
class and in every query you can implement a different order. That is one place. You don’t need to duplicate pagination code across multiple classes.