Builder Pattern in JavaScript/TypeScript
Pinesh I Menat
Posted on July 16, 2020
This article is not just about Builder Pattern in JS/TS, I will be explaining my thought process behind it and see if you can relate yourself to this. I believe everybody has a unique way of solving problems, hope you get something to learn from it.
A month ago I bought this very informative and useful Design Patterns course by Imtiaz Ahmed. Though this course is in Java, I implemented all the design patterns using Typescript. I will be discussing one important Assignment of this course, which is stated here,
Business Requirement:
Company needs a handy API they can use to interact with a search engine. You'll need to use the builder pattern to create the below JSON structure. This JSON structure is actually a query that can be submitted to a search engine called Elasticsearch. Elasticsearch is an opensource tool one can download and use but that's not important. Developers in our company using your builder API should be able to create JSON requests like this.
Here are the rules for the JSON structure:
- You can have a single must or should section inside a bool section as shown.
- But keep in mind that inside of each one of these musts or shoulds, you can have nested bool sections. The match section is simple, you can have any attribute name and it's value.
For example, the above JSON query is filtering for ONLY those items that are "Milk". And the "item_type" attribute has the value "Dairy". The product_location should be "New Mexico" with warehouse_number: 37.
You'll need to create a few classes that will represent this JSON structure when they are converted to a JSON format.
Create the classes called Query, Bool, Must, Match, and Test. You may also need a class called QueryBuilder, or whatever you want to name it. The client will invoke the builder methods to create the instances of the Musts, Shoulds, etc. and print out the composed objects JSON format to prove that the API works as expected.
Here is an example of how a developer expects to use the API:
QueryBuilder builder = new QueryBuilder();
builder.bool().mustMatch("item", "Milk").mustMatch("item_type", "Dairy");
builder.bool().shouldMatch("product_location","NewMexico").shouldMatch("warehouse_number", 37);
Remember the developers need to be able to nest bools inside of musts or shoulds if needed. So here's the case for a nested bool containing a must inside of an existing should section. The developers expect to use the API like this:
let builder: QueryBuilder = new QueryBuilder();
builder.bool().shouldMatch("lot_number",307).bool().mustMatch("expiry_date", "Jan 2020");
OR
builder.bool().mustMatch("item", "Milk").mustMatch("item_type", "Dairy");
builder.bool().shouldMatch("product_location", "New Mexico").shouldMatch("warehouse_number", 37);
Solution and My Thought Process:
So far all the builder patterns that I learned were very simple to understand and implement for example,
const myHouse = new HouseBuilder('Adder')
.setFloor(5)
.makeGarden()
.makeParking()
.build();
You can check out detailed implementation of House Builder on my github.One important thing to notice here is, the order in which we call different methods to build house doesn't really matter meaning, I can call setFloor(5) after makeGarden() as at the end when we call build method House obj is built. So the order is not important here but that's not the case for our assignment problem.
In our QueryBuilder assignment, we can have a single must or should section inside a bool section. And we can have nested bool sections inside of each one of these musts or shoulds. So the order here is very important and knowledge that I gained by learning the above HouseBuilder pattern was not enough to solve this problem.
It took me a lot of time to think through different solutions, I did not want to search online. I kept trying different approaches. Some of them are mentioned here,
- To validate the order in which the client can call API functions, I thought to use if-else but I gave up when I realized Query can be very nested and validating all this will be a lot of if-else which does not make sense, I moved on…
- I tried to achieve the same through Polymorphism but I did not work as I expected and I had to scrap this idea too.
Finally, I suck in the problem for a while and decided to reverse engineer it. Let me explain how,
Here the Query object is not built at last, unlike HouseBuilder where we call .build() method at the end of all method calls to create House obj. This got me into thinking that each time when we are calling QueryBuilder methods, each of these methods should return an object of class on which we should be able to call only specific methods that we will allow.
builder.bool().shouldMatch("lot_number",307).bool().mustMatch("expiry_date", "Jan 2020");
- Here QueryBulder should have a bool() method which will return an object of class Bool.
- And Bool class will have shoudMatch() and mustMatch() methods which will return obj of type Should and Must respectively.
- Now, this Should and Must class will have bool() method which will return obj of type Bool. so that we can have nested bool inside should or must in our query.
Let's dive into implementation code,
Hope you find this article helpful, could you please share your review in comments section ❤ Thank you.
Posted on July 16, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
February 11, 2022