TypeScript Bridge Design Pattern
What is bridge design pattern?
The Bridge design pattern is a structural design pattern. It’s a hierarchy breaker. It splits the large and complex hierarchy into two, which can then be developed independently from each other.
There are a few terminologies in the bridge design pattern. These are:
- Abstraction: It’s the high-level control logic that relies on the implementations of objects to perform low-level work.
- Implementor: It’s the interface that defines the requirements for concrete implementations. The abstract can only communicate with the concrete implementations via the implementor, which has the common methods defined in the implementor.
- Refined Abstraction: It’s an optional property that can extend the abstraction and create different variations of control logic.
- Concrete Implementations: These are the actual implementations of implementer interfaces.
When should the bridge design pattern be used?
It can be used when there is a need to extend the hierarchy in several different directions. It can also be used to reduce the complexity of a bigger hierarchy by breaking it into smaller pieces, then manipulating and altering these small pieces to create various types of use cases for the system. The bridge design pattern can also provide the ability to change the implementation at runtime without requiring an in-depth understanding of the underlying workings.
How to implement bridge design pattern in TypeScript
Let’s apply the Bridge design pattern to TypeScript. Imagine managing users across various database systems. We possess a complex hierarchy capable of registering and reading users. As our requirements grow, we opt to expand the databases we work with and introduce diverse user types into our system.
Given our intricate design, simply extending our existing system to meet these new requirements would necessitate adding classes to handle these specific needs. This approach risks complicating our design further, potentially leading to difficulties in adjusting dependencies to accommodate the new requirements.
Rather than navigating this path, we choose to implement the Bridge design pattern. This involves breaking down our complex hierarchy into smaller, manageable pieces, allowing us the freedom to handle new requirements and extend our system more seamlessly without overwhelming adjustments.
In our scenario, we break down our user registration and reading system to manage different user types, simplifying the extension of our capabilities, afterward we’re dividing our database logic to adapt to various database types.
Essentially, all we do is break our system into a high-level abstract definition and low-level workers, establishing a bridge between them based on our requirements.
Bridge design pattern diagram
Bridge design pattern code
// Implementor Interface
interface IUserImplementor {
getUser(id: number): string;
saveUser(id: number, name: string): void;
}
// Concrete Implementor
class MySQLImplementor implements IUserImplementor {
getUser(id: number): string {
return `MySQL: User with ID ${id}`;
}
saveUser(id: number, name: string): void {
console.log(`MySQL: Saving User - ID: ${id}, Name: ${name}`);
}
}
// Concrete Implementor
class MongoDBImplementor implements IUserImplementor {
getUser(id: number): string {
return `MongoDB: User with ID ${id}`;
}
saveUser(id: number, name: string): void {
console.log(`MongoDB: Saving User - ID: ${id}, Name: ${name}`);
}
}
// Abstraction Interface
interface IUserAbstraction {
getUserInfo(id: number): string;
saveUserInfo(id: number, name: string): void;
}
// Refined Abstraction
class BasicUser implements IUserAbstraction {
private implementor: IUserImplementor
constructor(implementor: IUserImplementor) {
this.implementor = implementor
}
getUserInfo(id: number): string {
return this.implementor.getUser(id);
}
saveUserInfo(id: number, name: string): void {
this.implementor.saveUser(id, name);
}
}
// Refined Abstraction
class PremiumUser implements IUserAbstraction {
private implementor: IUserImplementor
constructor(implementor: IUserImplementor) {
this.implementor = implementor
}
getUserInfo(id: number): string {
return `Premium ${this.implementor.getUser(id)}`;
}
saveUserInfo(id: number, name: string): void {
console.log(`Premium user: Saving User - ID: ${id}, Name: ${name}`);
this.implementor.saveUser(id, name);
}
}
// Client
const mysqlImplementor = new MySQLImplementor();
const basicUserMySQL = new BasicUser(mysqlImplementor);
// MySQL: User with ID 1
console.log(basicUserMySQL.getUserInfo(1));
// MySQL: Saving User - ID: 2, Name: ibrahim sengun
basicUserMySQL.saveUserInfo(2, 'ibrahim sengun');
const mongoImplementor = new MongoDBImplementor();
const premiumUserMongoDB = new PremiumUser(mongoImplementor);
// Premium MongoDB: User with ID 3
console.log(premiumUserMongoDB.getUserInfo(3));
// Premium user: Saving User - ID: 4, Name: james bond
premiumUserMongoDB.saveUserInfo(4, 'james bond');