Creational Design Patterns using Typescript
Creational Design Patterns are the category of design patterns that deal with object creation mechanisms.
This is the 2nd article in the series of Blog Posts for understanding Design Patterns using Typescript. SOLID Principles using Typescript was the first one in this series.
Creational Design Patterns are particularly important when the developer needs to incorporate some types of constraints on the creation of objects, for eg: having a single instance of an object or decoupling the instance created from where in the code it is used. This particularly helps resolve future complexity in software design.
By the end of this article, you’d have a clear idea, what are the creational design patterns, when are they applicable and which circumstance would require what type of Creational Design Pattern.
Primarily we have five types of creational design patterns:
- Singleton
- Prototype
- Factory Method
- Builder
- Abstract Factory
These are the types discussed in GoF Design Patterns. In addition to these is another important creational design pattern — Object Pool — used in the case of performance-boosting applications for complex class instantiations.
Let’s understand each of these design patterns (excluding Object Pool, which we’ll cover in another blog post) with practical examples. We’ll use Typescript as the language and favour practical examples over the theoretical ones you usually find online, involving classes such as box
, animal
, etc.
Singleton Design Pattern
The Singleton Design Pattern derives its origin from the mathematical concept of Singleton sets. This pattern restricts the instantiation of a class to just a single instance. It should be used very selectively and rarely since it’s mostly non-essential. One can even argue as to why we need a separate class at all, and not just a single variable or a set of global variables.
Problem
In most cases, the developer should avoid global variables and instances. But for cases such as logging — where logging is required with each function and does not change the state of the code — there is no need for instantiation of a class in every method of the codebase. So, to avoid unnecessary repetition and for performance gains, the Singleton pattern can be utilized.
Let’s consider a logger implementation as follows:
To utilize the above logger, a sample implementation can instantiate the Logger
class and utilize the logger.logError
method as seen in the following Gist.
The above code works well, but when you expand it to hundreds of different functions and classes, the unnecessary initializations suddenly start to feel redundant. One solution can be to inject the logger object everywhere. But again, it adds on as an unneeded parameter. apart from if the initializer is slow and complex, multiple initializations will start to have a compounding effect in a negative direction.
Solution
The best solution here is the Singleton Pattern. This is particularly useful in cases where the need for a global object is most desired. Let’s take a look at the implementation of the Logger
class as a Singleton
.
If you look at Line 7, the getInstance
returns the object of the class and it’s a static method. This is what creates the Singleton class. It restricts the creation of more than a single object of the class and returns the same object every time an instance is needed. Also, the constructor is private, restricting the implicit creation of a duplicate object.
This new Singleton class can be used as seen in the next Gist.
Summary
The Singleton Pattern on the surface seems like a way to use Global variables. But it has more implications when you start looking into it deeply. Firstly, the Singleton Pattern helps to have globally accessible actions instead of just variables. This is particularly useful in cases such as the above Logger. Other examples include cases like Database connection pools where you won’t need multitudes of connections.
One criticism this approach faces is that the global variables to which the Singleton Pattern is widely compared have mutable characteristics. But this should not be a problem if we keep in mind the fact that we do not want to change our unit test cases when applying the Singleton pattern.
Prototype Design Pattern
The Prototype Design Pattern is used when the creation of a new object is complex or expensive and a better approach is to clone the existing object.
Problem
The prototype pattern is typically used in cases where the creation of a new object is expensive or mutability is an issue. Let’s take a simple example to understand the problem.
For example, in a typical text editor or similar application, we have a feature to find a word in a blob of text.
From the above example, the first object creation takes 32 milliseconds, while the second one takes only 5–10 milliseconds. This is because of the optimizations provided by the compiler.
Solution
Since only a copy is needed to initialize, a better approach is to use a clone of the object. This can easily be done using the “Object.create” method.
Looking at line 28, instead of instantiation — the clone is created. This optimizes the code to generate a new object to less than 1ms, giving us an optimization of more than 400%+.
Summary
This pattern is generally not utilized in normal web-applications but becomes crucially important in cases where new object creation is complex or expensive. Typical cases such as instances where object creation is dependent on some network call that takes time might make use of Prototype Pattern. Other use cases include Gaming applications where items such as multiple new levels or items in the game such as cubes, doors etc. are created by cloning the original object.
Factory Method Design Pattern
The Factory Design Pattern deals with object creation separately from the client calling code. This is usually done to facilitate the DRY principle. Let’s just discuss this with an example.
Problem:
Suppose, there is a cybersecurity SAAS available with 2 plans: One is free with advertisements, and the second is the Pro plan with no advertisements. Since we’re pro-OOP — we start by creating a `Member` class and extend it for our cases.
Now it’s quite simple to use these classes and create members based on the type i.e. “free” or “pro”.
This code works fine and goes ahead with decent use of OOPs we know of. However, problems start cropping up when we need to add more cases to the above. Say the next requirement is to create a premium membership as well. Moreover, this function is doing multiple things at once — creating, registering and notifying. Most importantly, if the code is duplicated at multiple places, we’ve let go of the DRY principle.
Solution
Solving this is simple and can be thought of as extracting the repeated code into a common reusable function. This, in simple terms, is what the Factory method is doing. It extracts the common logic for object instantiation with reusable code.
In the above example, we simply create a MembershipFactory
class and add a static method encapsulating all the logic of the Member creation into a Factory class. Object creation is delegated and code duplication is avoided. The resulting code has only 1 place of change.
Summary
This pattern can simply be called “Simple Factory” and in some cases, it isn’t even described as a pattern since it’s simply an encapsulation of repeated code. Either way, it comes in pretty handy in such cases, regardless of whether or not we call it a pattern.
There are a few enhancement requirements that are not handled well by the Factory Method Pattern, for which we need to understand the applicability of the “Abstract Factory Method”.
Abstract Factory Method Design Pattern
Abstract Factory is more of an abstraction on the above-discussed Factory Method. With the Abstract Factory Method, we achieve encapsulation on object instantiation using abstract Factory classes, which results in the creation of objects of a similar theme.
Problem:
Let’s extend the example discussed previously with the Factory Method. Say we have a new requirement to have additional types of members but on similar themes of “Free” and “Pro”. Suppose the company was previously situated in the USA, and now plans to extend the product to European markets. We could use the same classes, but there are many more checks to be handled for European markets, like compliance. These types of object instantiations are already solved using the Abstract Factory Method pattern.
Solution
Before jumping into the solution, let’s quickly define some terms that are generally found in codebases or books to describe the Abstract Factory Design Pattern.
- Abstract Factory — Self Explanatory
- Concrete Factory — Implementation of Abstract Factory
- Abstract Product — Contract for Class derived from the Factory
- Concrete Product — Resulting Product from Factory Class Implementation
To solve this let’s take a bottom-up approach, understanding the Product class first, and then the abstraction around Factory Class.
Remember the Member
class from the Factory Method above. That’s our Product
class. Let’s now create EuropeanMember
with an agreement field.
Previously, Member
served as the abstract product, ProMember
and FreeMember
served as the default ConcreteProduct
(or Concrete Product for the USA in our case), EuropeanProMember
and EuropeanFreeMember
served as the Concrete Product classes for European markets.
Next, we need Factories to instantiate these concrete products. But before that, we need to refactor and create contracts for our Abstract Factory.
Now, finally, let’s add our Concrete Factory classes. We will be creating 2 concrete classes, USAMembershipFactory
and EuropeanMembershipFactory
that comply with the above Abstract Factory i.e. MembershipFactory
.
Using their own implementations, they can create particular types of members.
This makes it easier to achieve independent Product classes. The implementation is quite trivial in our case but becomes particularly useful in the case of multiple and complex business requirements.
Summary
Primarily Abstract Factories are used in conjunction with the Factory Method and are frequently used in codebases. The key point to keep in mind is that whenever there is a need for object instantiation of similar themes without specifying the concrete classes — Abstract Factory Method might be used.
Frequently, it can be encountered in implementations where we get an iterable that may be of the type `list`, or a custom class (you can iterate or get the length of the object) that conforms to the abstract product class and is a result of an implementation of an Abstract Factory.
Builder Design Pattern
The Builder Design Pattern is used for separating concerns of object creation from its UI representation.
Problem
Let’s take a typical example of an API. In our case, this API is built for a chess website. We have 2 typical classes/models for:
1. Players and 2. Match
Next, we try to fetch data using an API call and encapsulate the data using both Players
and ChessMatch
classes.
The fetchDetailsFromDB
the method can be thought of as a function that makes use of a typical ORM and returns the data in a specified model format i.e. in ChessMatch
object.
Normally, we rarely need data in the same format in UI as produced by the backend. Let’s say that in the above example, we need an isLive
flag in the UI that is true when the match is about to start in 5 minutes till the end of the match.
Solution
There are many approaches that come to mind when solving this particular problem. We shall discuss a few in this section:
Approach One: If it’s needed in the UI, then set it in the UI
This approach, though faster and seemingly obvious, hinders scalability. Think about a future requirement that needs the same output in both a web-app and a mobile app, saving 1 implementation effort doubles the work on the frontend. And what if the same API is being used in multiple places in both the web and mobile apps? With each increase in API usage, your incremental effort doubles.
Approach Two: Create a transformer/service layer
This is generally seen in many codebases in the backend where there is a mix of object-oriented and functional approaches being followed.
This approach is generally seen in more non-object-oriented/functional code and has developed into a pattern of its own.
Another way to make it more of an object-oriented approach (which does not reap any benefits, in my opinion) is the following —
The first transformer is basically transformed into a class method in the second approach and serves more or less the same purpose. Any of these approaches are incrementally better than the first approach. Personally, I would pick the first approach over the second, since it adds no unnecessary object-oriented abstraction.
Approach Three: The Builder Pattern
There is a better solution. When thinking about scalability and future requirements, we need to presume another set of requirements for the future.
For example, there are now 3 sets of new requirements:
1. Match Object with isLive
status on match Detail Page.
2. Match Object for Navbar without isLive
status, and with only player names.
Let’s take a look at the basic implementation and how it would for both the above API’s.
Looking at the above code with both API functions, we first instantiate the builder for each builder —
NavbarChessMatchBuilder
and DetailChessMatchBuilder
respectively.
So the flow for a typical builder pattern is as follows:
- Director assembles a particular instance for the resulting class (in our case
ChessMatch
instance) - Director then delegates the rest of the building to the particular Builder class (
NavbarChessMatchBuilder
andDetailChessMatchBuilder
)
3. The Builder can give the final result based on its particular specification.
Let’s take a closer look at the Director class for our particular example:
The Director class has only 2 specific functions:
1. Assemble the initial result class.
This is done in line 5 with new ChessMatch
instantiated.
2. Delegate the rest of the building to a particular builder class.
This is done using the construct
function.
Lastly, let’s take a closer look at the Builder classes which are extended from the abstract ChessMatchBuilder
in our use-case.
In DetailChessMatchBuilder
we needed the isLive
status that has been added in line 23. While we have no such requirement for the match status on Navbar, we do need lesser information from player objects (i.e. only 3 keys). The result is modified accordingly on line 11.
Summary
As you can see from the previous example, the Builder pattern helps us in the separation of concerns for object instantiation from its representation. The object-oriented way of `transformer` is far more cleaner than it’s non-object-oriented counterparts.
The need for a Director class is not compulsory but having similar names and a structure helps all the developers to quickly get in sync with what’s happening inside huge codebases.
Conclusion
In this part, we’ve summarised all of GoF Creational Design Patterns using practical examples and clear explanation with Typescript. Creational Design Patterns cannot be ignored in the case of applications ranging from medium to large backends with decent complexity and scalability problems. These help us stand on the shoulders of the giants who have already solved this problem with extreme diligence.