In software design, a design pattern is a general solution to a commonly occurring problem. Design patterns are mechanisms for thinking about how different objects in your code will interact. They’re not specific code implementations. Instead, they’re templates for how those interactions might work.
Design patterns address challenges that arise frequently when working with code and reduce the time spent on repeatedly solving the same problems.
Design patterns are popularly classified based on their purpose, which can be creational, structural, and behavioral. In this article, we will cover some of the most commonly used creational and structural design patterns in TypeScript.
Creational design patterns are patterns primarily concerned with the process of object creation. They abstract the object instantiation process and make a system independent of how objects are created, composed, and represented.
Creational patterns are mainly used in evolving systems that depend more on object composition than class inheritance. Examples include the factory pattern, the singleton pattern, etc.
Factory Method Pattern
The Factory method pattern, also known as the virtual constructor design pattern, is a creational pattern that loosens the coupling of an application by separating its construction code from the code that uses the application.
The factory pattern creates a general “factory/creator” class and allows its subclasses to choose the kind of object they want to instantiate.
This pattern is implemented by creating an interface all the products will follow, then, creating a general abstract creator class for all other creators (concrete creators) to extend. Finally, creating concrete products that implement the product interface.
Below is a code example of a chair factory producing wooden and plastic chairs.
Implementing this pattern allows you to extend your codebase without breaking it. For example, add any type of chair, like a leather chair.
This pattern should only be used when the type of objects your code will use is unknown because the pattern makes it easy to extend the product construction code independently from the rest of the application.
Although this pattern allows you to avoid tight coupling between the creator class and the concrete products and maintains the single responsibility and open/closed principle, the code can become very complicated due to the number of subclasses needed to implement the pattern.
The singleton pattern is a creational pattern that allows you to restrict access to a shared resource by ensuring that a class can only be instantiated once and providing a global point of access to it.
The singleton pattern makes the class constructor private because, by default, the constructor will always return a new object, defeating the whole idea of the pattern.
Then create a static method to check if the class has already been instantiated. If it has been instantiated before, it returns the already created instance; if it hasn’t, it creates an instance of the class and saves it to a static instance variable.
Implementing the Singleton pattern in TypeScript, you’d have:
Although the Singleton pattern provides a global access point to the only instance of a class; it is a very controversial pattern and is considered an anti-pattern by many engineers who advise against using it because it violates the single-responsibility principle.
The pattern also doesn't work well in multi-threaded environments as many components may be trying to access the shared resources simultaneously.
Structural design patterns are patterns primarily concerned with how objects are composed to form larger structures.
They are mainly used for making independently developed class libraries work together.
Examples include the adapter pattern, the facade pattern, etc.
The adapter design pattern, also known as the wrapper design pattern, is a structural pattern that allows incompatible interfaces to interact by creating an adapter. An adapter is a middle layer class that serves as a bridge or translator between two incompatible interfaces.
An adapter works by implementing an interface of one of the incompatible objects and, through that interface, calls the methods of the second object and then passes the request to the second object in a format it would understand.
For context, assume you need to plug a three-pin plug into a two-pin wall socket. You would need a socket “adapter” that “implements” a two-pinned system and exposes a three-pinned socket, allowing you to plug your initial three-pin plug. This is how the adapter pattern works too.
For example, say you built an application that accepts data in XML format. Later on, you need to consume data from a third-party library that sends JSON data. Changing the code of your initial application might break some things; the better way to approach this would be to use the adapter pattern.
Implementing this scenario in code, you’d have:
Recall that your application only accepts XML data, but it needs to accept JSON data too. Therefore, your application is the target, and the application that serves JSON data is the adaptee. So you create a JSON to XML adapter which implements the target’s interface and calls the adaptee’s method safely. Therefore, giving your application a way to receive JSON data without changing your source code.
Although this pattern obeys the single responsibility and open/close principle, it can increase the overall complexity of your code. So it should only be used when you need to use an already existing class with an incompatible interface with the rest of your code.
The Facade Pattern is a structural pattern that introduces a facade object that offers a simplified interface to a complicated set of classes like a framework or library, reducing communication and dependencies across subsystems.
The facade object works by taking parts of complex classes required for an implementation to work and calling them inside it.
For example, assume you are creating an E-commerce application and implementing the logic for placing an order. First, the customer has to be logged in, then the product the customer wants has to be in the store, and finally, the customer has to have enough money to purchase the product. Each of these processes is handled by three different classes, and these classes might contain other methods that are not relevant to the client for the placing-order process.
Implementing the facade pattern by taking all the required methods and creating a facade object would be optimal for this scenario.
Implementing this scenario in code, you’d have:
The facade pattern should only be used when you need a simpler and straightforward interface to complex subsystems or when you want to structure subsystems into layers.
Although the facade pattern allows you to isolate your code from a complex subsystem, a facade object can become a god object coupled to all of an application’s classes.
In this article, you learned about the factory method, singleton, adapter, and facade design patterns implemented in TypeScript. Having these design patterns in your development arsenal will help you solve some common design problems and communicate effectively with other developers in collaborative projects.