Prerequisites
- Basic understanding of JavaScript ๐
- TypeScript installation โก
- VS Code or preferred IDE ๐ป
What you'll learn
- Understand the concept fundamentals ๐ฏ
- Apply the concept in real projects ๐๏ธ
- Debug common issues ๐
- Write type-safe code โจ
๐ฏ Introduction
Welcome to this exciting tutorial on the Bridge Pattern! ๐ In this guide, weโll explore how to separate abstraction from implementation in TypeScript, giving your code incredible flexibility and maintainability.
Youโll discover how the Bridge Pattern can transform your TypeScript development by decoupling what something does from how it does it. Whether youโre building UI components ๐จ, device drivers ๐ฑ, or cross-platform applications ๐, understanding the Bridge Pattern is essential for writing extensible, maintainable code.
By the end of this tutorial, youโll feel confident using the Bridge Pattern in your own projects! Letโs dive in! ๐โโ๏ธ
๐ Understanding Bridge Pattern
๐ค What is the Bridge Pattern?
The Bridge Pattern is like having a universal remote control ๐ฎ that can work with any TV brand. Think of it as creating a bridge between what you want to do (change channel, adjust volume) and how different TVs actually do it (Samsungโs way, Sonyโs way, LGโs way).
In TypeScript terms, the Bridge Pattern separates an abstraction from its implementation so both can vary independently. This means you can:
- โจ Add new abstractions without changing implementations
- ๐ Add new implementations without changing abstractions
- ๐ก๏ธ Avoid a cartesian product of classes
๐ก Why Use the Bridge Pattern?
Hereโs why developers love the Bridge Pattern:
- Flexibility ๐ง: Change implementations at runtime
- Extensibility ๐: Add new features without breaking existing code
- Clean Architecture ๐๏ธ: Separate concerns effectively
- Reduced Complexity ๐ฏ: Avoid class explosion
Real-world example: Imagine building a notification system ๐ฌ. With the Bridge Pattern, you can send notifications (email, SMS, push) through different platforms (AWS, Twilio, Firebase) without creating a class for each combination!
๐ง Basic Syntax and Usage
๐ Simple Example
Letโs start with a friendly example:
// ๐ Hello, Bridge Pattern!
// ๐จ Define the implementation interface
interface DeviceImplementation {
turnOn(): void;
turnOff(): void;
setVolume(percent: number): void;
}
// ๐ฎ Create the abstraction
abstract class RemoteControl {
protected device: DeviceImplementation;
constructor(device: DeviceImplementation) {
this.device = device;
}
abstract togglePower(): void;
abstract volumeUp(): void;
abstract volumeDown(): void;
}
// ๐บ Concrete implementation
class TV implements DeviceImplementation {
private isOn = false;
private volume = 50;
turnOn(): void {
this.isOn = true;
console.log("๐บ TV is ON! Welcome! ๐");
}
turnOff(): void {
this.isOn = false;
console.log("๐บ TV is OFF. Goodbye! ๐");
}
setVolume(percent: number): void {
this.volume = percent;
console.log(`๐บ TV volume: ${percent}% ๐`);
}
}
๐ก Explanation: Notice how we separate what a remote does (abstraction) from how devices work (implementation). The remote doesnโt know if itโs controlling a TV or radio!
๐ฏ Common Patterns
Here are patterns youโll use daily:
// ๐๏ธ Pattern 1: Basic remote
class BasicRemote extends RemoteControl {
private powerOn = false;
togglePower(): void {
if (this.powerOn) {
this.device.turnOff();
this.powerOn = false;
} else {
this.device.turnOn();
this.powerOn = true;
}
}
volumeUp(): void {
console.log("๐ Volume Up!");
this.device.setVolume(60);
}
volumeDown(): void {
console.log("๐ Volume Down!");
this.device.setVolume(40);
}
}
// ๐จ Pattern 2: Advanced remote with more features
class AdvancedRemote extends RemoteControl {
private powerOn = false;
private currentVolume = 50;
togglePower(): void {
if (this.powerOn) {
this.device.turnOff();
this.powerOn = false;
} else {
this.device.turnOn();
this.powerOn = true;
}
}
volumeUp(): void {
this.currentVolume = Math.min(100, this.currentVolume + 10);
this.device.setVolume(this.currentVolume);
}
volumeDown(): void {
this.currentVolume = Math.max(0, this.currentVolume - 10);
this.device.setVolume(this.currentVolume);
}
// ๐ฏ Advanced feature
mute(): void {
console.log("๐ Muted!");
this.device.setVolume(0);
}
}
๐ก Practical Examples
๐ Example 1: Payment Processing System
Letโs build something real:
// ๐๏ธ Define payment method interface
interface PaymentMethod {
processPayment(amount: number): boolean;
validateCredentials(): boolean;
getTransactionFee(): number;
}
// ๐ณ Abstract payment processor
abstract class PaymentProcessor {
protected paymentMethod: PaymentMethod;
constructor(paymentMethod: PaymentMethod) {
this.paymentMethod = paymentMethod;
}
abstract makePayment(amount: number): void;
abstract refund(amount: number): void;
}
// ๐ฐ Concrete payment methods
class CreditCard implements PaymentMethod {
private cardNumber: string;
constructor(cardNumber: string) {
this.cardNumber = cardNumber;
}
processPayment(amount: number): boolean {
console.log(`๐ณ Processing $${amount} via Credit Card`);
return true;
}
validateCredentials(): boolean {
console.log("โ
Card validated!");
return this.cardNumber.length === 16;
}
getTransactionFee(): number {
return 2.9; // 2.9% fee
}
}
class PayPal implements PaymentMethod {
private email: string;
constructor(email: string) {
this.email = email;
}
processPayment(amount: number): boolean {
console.log(`๐
ฟ๏ธ Processing $${amount} via PayPal`);
return true;
}
validateCredentials(): boolean {
console.log("โ
PayPal account verified!");
return this.email.includes("@");
}
getTransactionFee(): number {
return 3.4; // 3.4% fee
}
}
// ๐ Basic checkout processor
class BasicCheckout extends PaymentProcessor {
makePayment(amount: number): void {
if (this.paymentMethod.validateCredentials()) {
const fee = (amount * this.paymentMethod.getTransactionFee()) / 100;
const total = amount + fee;
console.log(`๐ฐ Subtotal: $${amount}`);
console.log(`๐ Fee: $${fee.toFixed(2)}`);
console.log(`๐ต Total: $${total.toFixed(2)}`);
if (this.paymentMethod.processPayment(total)) {
console.log("๐ Payment successful!");
}
}
}
refund(amount: number): void {
console.log(`๐ธ Refunding $${amount}`);
}
}
// ๐ Express checkout with saved details
class ExpressCheckout extends PaymentProcessor {
private savedDetails = true;
makePayment(amount: number): void {
if (this.savedDetails) {
console.log("โก Using saved payment details!");
if (this.paymentMethod.processPayment(amount)) {
console.log("๐ Express payment complete!");
}
} else {
console.log("๐ Please save payment details first");
}
}
refund(amount: number): void {
console.log(`โก Express refund of $${amount} initiated!`);
}
}
// ๐ฎ Let's use it!
const creditCard = new CreditCard("1234567890123456");
const paypal = new PayPal("[email protected]");
const basicCheckout = new BasicCheckout(creditCard);
basicCheckout.makePayment(99.99);
const expressCheckout = new ExpressCheckout(paypal);
expressCheckout.makePayment(49.99);
๐ฏ Try it yourself: Add a cryptocurrency payment method and a subscription processor!
๐ฎ Example 2: Multi-Platform Messaging System
Letโs make it fun:
// ๐ฌ Message sender interface
interface MessageSender {
sendText(message: string): void;
sendImage(url: string): void;
sendVideo(url: string): void;
getDeliveryStatus(): string;
}
// ๐ฌ Abstract messaging app
abstract class MessagingApp {
protected sender: MessageSender;
constructor(sender: MessageSender) {
this.sender = sender;
}
abstract sendMessage(content: string): void;
abstract sendMedia(type: "image" | "video", url: string): void;
abstract checkStatus(): void;
}
// ๐ฑ Platform implementations
class WhatsAppSender implements MessageSender {
sendText(message: string): void {
console.log(`๐ฑ WhatsApp: ${message} โ
โ
`);
}
sendImage(url: string): void {
console.log(`๐ท WhatsApp Image: ${url} ๐ผ๏ธ`);
}
sendVideo(url: string): void {
console.log(`๐ฌ WhatsApp Video: ${url} ๐น`);
}
getDeliveryStatus(): string {
return "โ
โ
Delivered";
}
}
class SlackSender implements MessageSender {
private workspace = "awesome-team";
sendText(message: string): void {
console.log(`๐ผ Slack [${this.workspace}]: ${message}`);
}
sendImage(url: string): void {
console.log(`๐ผ๏ธ Slack Image uploaded: ${url}`);
}
sendVideo(url: string): void {
console.log(`๐น Slack Video shared: ${url}`);
}
getDeliveryStatus(): string {
return "๐ Seen by 5 people";
}
}
// ๐ฏ Basic messaging
class BasicMessenger extends MessagingApp {
sendMessage(content: string): void {
console.log("๐จ Sending message...");
this.sender.sendText(content);
}
sendMedia(type: "image" | "video", url: string): void {
if (type === "image") {
this.sender.sendImage(url);
} else {
this.sender.sendVideo(url);
}
}
checkStatus(): void {
console.log(`๐ Status: ${this.sender.getDeliveryStatus()}`);
}
}
// ๐ Business messaging with features
class BusinessMessenger extends MessagingApp {
private messageQueue: string[] = [];
sendMessage(content: string): void {
// Add business header
const businessMessage = `[Company Update] ${content}`;
console.log("๐ข Sending business message...");
this.sender.sendText(businessMessage);
this.logMessage(businessMessage);
}
sendMedia(type: "image" | "video", url: string): void {
console.log("๐ Checking media compliance...");
if (type === "image") {
this.sender.sendImage(url);
} else {
this.sender.sendVideo(url);
}
this.logMessage(`Media sent: ${type} - ${url}`);
}
checkStatus(): void {
console.log(`๐ Business Dashboard: ${this.sender.getDeliveryStatus()}`);
console.log(`๐ Messages sent today: ${this.messageQueue.length}`);
}
private logMessage(message: string): void {
this.messageQueue.push(message);
console.log("๐พ Message logged for compliance");
}
bulkSend(messages: string[]): void {
console.log(`๐ค Sending ${messages.length} messages in bulk...`);
messages.forEach((msg, index) => {
setTimeout(() => {
this.sendMessage(msg);
}, index * 1000);
});
}
}
๐ Advanced Concepts
๐งโโ๏ธ Advanced Topic 1: Dynamic Bridge Switching
When youโre ready to level up, try this advanced pattern:
// ๐ฏ Advanced theme system with dynamic switching
interface ThemeRenderer {
renderButton(text: string): string;
renderCard(content: string): string;
getColorScheme(): { primary: string; secondary: string };
}
// ๐จ Multiple theme implementations
class DarkTheme implements ThemeRenderer {
renderButton(text: string): string {
return `<button class="dark-btn">๐ ${text}</button>`;
}
renderCard(content: string): string {
return `<div class="dark-card">๐ ${content}</div>`;
}
getColorScheme() {
return { primary: "#1a1a1a", secondary: "#333333" };
}
}
class LightTheme implements ThemeRenderer {
renderButton(text: string): string {
return `<button class="light-btn">โ๏ธ ${text}</button>`;
}
renderCard(content: string): string {
return `<div class="light-card">๐ ${content}</div>`;
}
getColorScheme() {
return { primary: "#ffffff", secondary: "#f0f0f0" };
}
}
// ๐ช Dynamic UI component with theme switching
class UIComponent {
private theme: ThemeRenderer;
constructor(theme: ThemeRenderer) {
this.theme = theme;
}
// ๐ Dynamic theme switching
switchTheme(newTheme: ThemeRenderer): void {
console.log("๐จ Switching theme...");
this.theme = newTheme;
this.render();
}
render(): void {
const colors = this.theme.getColorScheme();
console.log(`๐จ Theme colors: ${colors.primary} / ${colors.secondary}`);
console.log(this.theme.renderButton("Click me!"));
console.log(this.theme.renderCard("Beautiful content"));
}
}
๐๏ธ Advanced Topic 2: Bridge with Factory Pattern
For the brave developers:
// ๐ Database abstraction with factory
interface DatabaseConnection {
connect(): void;
query(sql: string): any[];
disconnect(): void;
}
interface DatabaseFactory {
createConnection(): DatabaseConnection;
}
// ๐๏ธ Multiple database implementations
class PostgreSQLConnection implements DatabaseConnection {
connect(): void {
console.log("๐ Connected to PostgreSQL!");
}
query(sql: string): any[] {
console.log(`๐ PostgreSQL executing: ${sql}`);
return [{ id: 1, data: "๐ฏ PostgreSQL data" }];
}
disconnect(): void {
console.log("๐ PostgreSQL disconnected");
}
}
class MongoDBConnection implements DatabaseConnection {
connect(): void {
console.log("๐ Connected to MongoDB!");
}
query(sql: string): any[] {
console.log(`๐ MongoDB executing: ${sql}`);
return [{ _id: "abc", data: "๐ MongoDB data" }];
}
disconnect(): void {
console.log("๐ MongoDB disconnected");
}
}
// ๐ญ Database client with bridge
abstract class DatabaseClient {
protected connection: DatabaseConnection;
protected factory: DatabaseFactory;
constructor(factory: DatabaseFactory) {
this.factory = factory;
this.connection = factory.createConnection();
}
abstract executeQuery(query: string): void;
abstract performTransaction(queries: string[]): void;
}
// ๐ฏ Implementation
class StandardDatabaseClient extends DatabaseClient {
executeQuery(query: string): void {
this.connection.connect();
const results = this.connection.query(query);
console.log("๐ Results:", results);
this.connection.disconnect();
}
performTransaction(queries: string[]): void {
this.connection.connect();
console.log("๐ Starting transaction...");
queries.forEach(q => this.connection.query(q));
console.log("โ
Transaction complete!");
this.connection.disconnect();
}
}
โ ๏ธ Common Pitfalls and Solutions
๐ฑ Pitfall 1: Tight Coupling
// โ Wrong way - abstraction knows too much about implementation!
class BadRemote {
private tv: TV; // ๐ฐ Coupled to specific implementation!
constructor() {
this.tv = new TV(); // ๐ฅ Can't use with Radio!
}
}
// โ
Correct way - use interface!
class GoodRemote {
private device: DeviceImplementation; // ๐ก๏ธ Works with any device!
constructor(device: DeviceImplementation) {
this.device = device;
}
}
๐คฏ Pitfall 2: Forgetting to Bridge Both Directions
// โ Dangerous - one-way bridge!
interface Printer {
print(doc: string): void;
}
class Document {
print(printer: Printer): void {
printer.print(this.content); // ๐ฅ No abstraction for Document!
}
}
// โ
Safe - proper two-way bridge!
interface Printer {
print(doc: DocumentAbstraction): void;
}
abstract class DocumentAbstraction {
abstract getContent(): string;
abstract format(): string;
}
class PDFDocument extends DocumentAbstraction {
getContent(): string {
return "๐ PDF content";
}
format(): string {
return "PDF";
}
}
๐ ๏ธ Best Practices
- ๐ฏ Keep Interfaces Focused: Donโt create huge interfaces
- ๐ Use Clear Names:
Implementation
suffix helps clarity - ๐ก๏ธ Program to Interfaces: Always use abstractions in client code
- ๐จ Separate Concerns: One responsibility per class
- โจ Consider Composition: Bridge often works with other patterns
๐งช Hands-On Exercise
๐ฏ Challenge: Build a Drawing Application
Create a type-safe drawing application with bridges:
๐ Requirements:
- โ Support multiple shapes (Circle, Rectangle, Triangle)
- ๐ท๏ธ Support multiple renderers (SVG, Canvas, ASCII)
- ๐ค Each shape can be drawn by any renderer
- ๐ Add color support to shapes
- ๐จ Each renderer handles colors differently!
๐ Bonus Points:
- Add animation support
- Implement shape transformations
- Create a composite shape pattern
๐ก Solution
๐ Click to see solution
// ๐ฏ Our bridge pattern drawing system!
interface Renderer {
renderCircle(x: number, y: number, radius: number, color: string): void;
renderRectangle(x: number, y: number, width: number, height: number, color: string): void;
renderTriangle(x1: number, y1: number, x2: number, y2: number, x3: number, y3: number, color: string): void;
}
// ๐จ Abstract shape
abstract class Shape {
protected renderer: Renderer;
protected color: string;
constructor(renderer: Renderer, color: string = "black") {
this.renderer = renderer;
this.color = color;
}
abstract draw(): void;
abstract move(dx: number, dy: number): void;
setColor(color: string): void {
this.color = color;
console.log(`๐จ Color changed to ${color}`);
}
}
// ๐ข Circle implementation
class Circle extends Shape {
constructor(
private x: number,
private y: number,
private radius: number,
renderer: Renderer,
color: string = "red"
) {
super(renderer, color);
}
draw(): void {
console.log(`โญ Drawing circle at (${this.x}, ${this.y})`);
this.renderer.renderCircle(this.x, this.y, this.radius, this.color);
}
move(dx: number, dy: number): void {
this.x += dx;
this.y += dy;
console.log(`โก๏ธ Circle moved to (${this.x}, ${this.y})`);
}
}
// ๐ฆ Rectangle implementation
class Rectangle extends Shape {
constructor(
private x: number,
private y: number,
private width: number,
private height: number,
renderer: Renderer,
color: string = "blue"
) {
super(renderer, color);
}
draw(): void {
console.log(`โฌ Drawing rectangle at (${this.x}, ${this.y})`);
this.renderer.renderRectangle(this.x, this.y, this.width, this.height, this.color);
}
move(dx: number, dy: number): void {
this.x += dx;
this.y += dy;
console.log(`โก๏ธ Rectangle moved to (${this.x}, ${this.y})`);
}
}
// ๐ผ๏ธ SVG Renderer
class SVGRenderer implements Renderer {
renderCircle(x: number, y: number, radius: number, color: string): void {
console.log(`๐ผ๏ธ SVG: <circle cx="${x}" cy="${y}" r="${radius}" fill="${color}" />`);
}
renderRectangle(x: number, y: number, width: number, height: number, color: string): void {
console.log(`๐ผ๏ธ SVG: <rect x="${x}" y="${y}" width="${width}" height="${height}" fill="${color}" />`);
}
renderTriangle(x1: number, y1: number, x2: number, y2: number, x3: number, y3: number, color: string): void {
console.log(`๐ผ๏ธ SVG: <polygon points="${x1},${y1} ${x2},${y2} ${x3},${y3}" fill="${color}" />`);
}
}
// ๐จ Canvas Renderer
class CanvasRenderer implements Renderer {
renderCircle(x: number, y: number, radius: number, color: string): void {
console.log(`๐จ Canvas: ctx.beginPath(); ctx.arc(${x}, ${y}, ${radius}, 0, 2*Math.PI);`);
console.log(`๐จ Canvas: ctx.fillStyle = "${color}"; ctx.fill();`);
}
renderRectangle(x: number, y: number, width: number, height: number, color: string): void {
console.log(`๐จ Canvas: ctx.fillStyle = "${color}";`);
console.log(`๐จ Canvas: ctx.fillRect(${x}, ${y}, ${width}, ${height});`);
}
renderTriangle(x1: number, y1: number, x2: number, y2: number, x3: number, y3: number, color: string): void {
console.log(`๐จ Canvas: ctx.beginPath(); ctx.moveTo(${x1}, ${y1});`);
console.log(`๐จ Canvas: ctx.lineTo(${x2}, ${y2}); ctx.lineTo(${x3}, ${y3}); ctx.closePath();`);
console.log(`๐จ Canvas: ctx.fillStyle = "${color}"; ctx.fill();`);
}
}
// ๐ฎ Test it out!
const svgRenderer = new SVGRenderer();
const canvasRenderer = new CanvasRenderer();
const circle = new Circle(50, 50, 30, svgRenderer);
circle.draw();
circle.setColor("purple");
circle.draw();
const rectangle = new Rectangle(100, 100, 60, 40, canvasRenderer);
rectangle.draw();
rectangle.move(10, 10);
rectangle.draw();
// ๐ Switch renderer at runtime!
console.log("\n๐ Switching renderers...\n");
const anotherCircle = new Circle(0, 0, 25, canvasRenderer, "green");
anotherCircle.draw();
๐ Key Takeaways
Youโve learned so much! Hereโs what you can now do:
- โ Create Bridge Patterns with confidence ๐ช
- โ Separate abstractions from implementations effectively ๐ก๏ธ
- โ Apply the pattern in real projects ๐ฏ
- โ Avoid common mistakes that trip up beginners ๐
- โ Build flexible systems with TypeScript! ๐
Remember: The Bridge Pattern is about flexibility and independence. Keep your abstractions and implementations separate! ๐ค
๐ค Next Steps
Congratulations! ๐ Youโve mastered the Bridge Pattern!
Hereโs what to do next:
- ๐ป Practice with the exercises above
- ๐๏ธ Build a notification system using Bridge Pattern
- ๐ Move on to our next tutorial: Composite Pattern
- ๐ Share your Bridge Pattern implementations with others!
Remember: Every TypeScript expert was once a beginner. Keep coding, keep learning, and most importantly, have fun! ๐
Happy coding! ๐๐โจ