Typescript, Design Patterns, and You
As with any craft, mastery takes practice, patience, and more practice. Writing code is no different, recognizing when to use what pattern or how to get the most out of a programming language just takes time. Junior and senior developers alike regularly contend with questions like how to write clean code, how and when to optimize, what patterns to use and when to use them. Although you can only get a feel for it with experience, the good news is that most of the common problems that devs face have been solved to a certain degree.
In this article I will discuss a few common design patterns to help you produce clean and readable code. Although I will use Typescript as an example, you can apply these patterns to most languages
Builders 🛠
The builder pattern is one of the most commonly used design patterns. Many libraries use it to build out complex objects that can take a variable number of parameters at any given time.
Refactoring Guru has this great definition for the builder pattern:
“Builder is a creational design pattern that lets you construct complex objects step by step. The pattern allows you to produce different types and representations of an object using the same construction code.”
If that doesn’t mean much to you and you still have no idea when to use the builder pattern, no worries, that’s a pretty long winded definition. The way I approach it is that if I realize my constructor is getting too large, chances are I need a builder.
In our case we had an aggregation layer service that had to call a microservice. The request sent out by our aggregation layer can vary wildly based on the parameters passed in. The creation got kind of complex, had a lot of conditions based on the inclusion or exclusion of other parameters, and could grow at any time. This seemed like the perfect use case for a builder!
Let’s take a look at some sample code:
As we can see here, we have a search body that can take a lot of parameters, some are needed, some aren’t, some affect others and some don’t.
If you were trying to build this with ‘if/else’ statements you would quickly lose your mind, however introducing a builder makes creating any type of request super simple.
Let’s see how we can create different types of queries:
This will generate a URL that will include the following:
- A language
- A country code
- The gender
- Recommendation type
- What algorithm to use
- The categories to filter by
- The brands to filter by
- The query to filter by
- The amount of results we want back
This can be as simple or as complicated as you need. Here is an example with just a few query parameters.
This will generate a URL that will include only:
- A language
- A country code
- A related query
- The amount of results we want back
As you can imagine, you can add as many or as few query parameters as you’d like, as long as the minimum contract of the constructor is respected. This can create queries as short as:
recommendations/entity/brands/algorithm/new_arrivals?country=us&language=en&gender=women
Or as long as:
/recommendations/entity/products/algorithm/trending?gender=men&language=en&country=us&count=20&page=1&version=v1.0&isSalesOnly=false&personalized=true&query=red%20shoes
Chain of Responsibility ⛓
This pattern is one of my personal favorites. I don’t know why we don’t see it used more often as it’s a great tool to help simplify long and complex steps and workflows. The most common use case I have seen of this is when defining middlewares for your node server to handle. Whenever you have defined a set of middlewares to use, and you either throw an exception or forward the request within it, you have in fact used a variant of this pattern.
If you ever catch yourself coding a bunch of sequential steps and handling business logic using ‘if/else’ statements, then this might be the right pattern for you. One thing to note is that in the Chain of Responsibility, it is completely acceptable not to complete the whole chain and terminate early.
Let’s take a common use case. A user wants to sign up for an account on ssense.com. When this happens we want to take the following steps:
- Check if their email exists. If it does, redirect them to a login page, nothing else happens.
- A user’s email doesn’t exist so we want to save it in our database.
- We send out an email welcoming them.
- We send their info over to our mailing service.
Here is how we can code this using Chain of Responsibility:
As you can see, each part of the chain adheres to the same interface and keeps track of the request that is supposed to follow. Each part of the chain is easily testable and changing the order or adding new steps is trivial.
Dependency Injection
If you are coming from the Java world and have used Spring before, you know Dependency Injection is just… well…
In Typescript you can also achieve dependency injection relatively easily thanks to one of my personal favorite libraries: InversifyJs.
Dependency Injection is one of the most important concepts of software engineering, where we depend on an abstraction as opposed to an implementation. This allows us to make the code significantly easier to test, maintain, and develop. You reduce coupling and allow to switch the functionality of your code with ease.
Let’s take a look at a concrete example that we have at SSENSE. Let’s say we have two data services that can serve product information: a catalog service and a search service. One is useful when we know exactly the product we want, and the other is useful when we want to do a search using some query parameters. Using dependency injection and InversifyJs, we can easily abstract away the implementation of both, and just serve a common interface to get the information from one example or another.
First, let’s “bootstrap” our dependencies. All this means is that we will define when to use an exact concrete implementation.
To translate this into English, what we are saying is:
- When we want a gateway and the specific one is a product, then create an instance of ProductGateway.
- When we want a gateway and the specific one is to search, then create an instance of SearchGateway
This is extremely important, as it means we will not need to new-up these classes when we use them.
Let’s take a look at an example:
And voila! Earlier, we defined how to do the lookup for the components we want to inject and now, here is how we access them. The best part in all this, is that at any point in time, we can change the implementation of the product or search gateways to point to a whole new service, and we never have to come back to the ProductService to make any changes to the code.
This is also extremely useful when testing, as now you can pass in a mock of the gateways and this service is none the wiser.
Well there you have it, 3 common design patterns to try out. At SSENSE we strive for high code quality and software that we can maintain and scale. Being aware of common design patterns and good coding practices goes a long way in creating a healthy codebase that everyone can work with.
Editorial reviews by Deanna Chow, Liela Touré & Prateek Sanyal.
Want to work with us? Click here to see all open positions at SSENSE!