Bridge

Bridge Pattern

Introduction

The Bridge pattern is a structural design pattern that decouples an abstraction from its implementation so that the two can vary independently. It is particularly useful when both the abstraction and its implementation need to be extended using subclasses.

Purpose

  • To separate an object’s abstraction from its implementation so that the two can vary independently.
  • To avoid a permanent binding between an abstraction and its implementation.

Use Cases

  • When you want to avoid a permanent binding between an abstraction and its implementation. This might be the case, for example, when they need to be selected or switched at runtime.
  • When both the abstractions and their implementations should be extensible through subclassing.
  • To hide the implementation details from the client.

Advantages/Disadvantages

Advantages:

  • Enhances the extensibility of both abstractions and implementations.
  • Hides implementation details from clients.
  • Supports the principle of composition over inheritance.

Disadvantages:

  • Can increase complexity by adding an additional layer of abstraction.
  • Requires a correct understanding of the initial architecture to be implemented effectively.

Implementation in JavaScript

javascript
	class Abstraction {
	  constructor(implementation) {
	    this.implementation = implementation;
	  }
	
	  operation() {
	    const result = this.implementation.operationImplementation();
	    return `Abstraction: Base operation with:\n${result}`;
	  }
	}
	
	class Implementation {
	  operationImplementation() {
	    return 'Implementation: Concrete Implementation';
	  }
	}
	
	class ExtendedAbstraction extends Abstraction {
	  operation() {
	    const result = this.implementation.operationImplementation();
	    return `ExtendedAbstraction: Extended operation with:\n${result}`;
	  }
	}
	
	// Usage
	const implementation = new Implementation();
	const abstraction = new Abstraction(implementation);
	console.log(abstraction.operation());
	
	const extendedAbstraction = new ExtendedAbstraction(implementation);
	console.log(extendedAbstraction.operation());

Practical Example

Consider a scenario where you’re developing a logging system that should support different output formats (like console, file, network) and different levels of logging (like info, debug, error).

javascript
	// Implementor
	class LoggerOutput {
	  write(message) {}
	}
	
	// Concrete Implementors
	class ConsoleLoggerOutput extends LoggerOutput {
	  write(message) {
	    console.log(`Console: ${message}`);
	  }
	}
	
	class FileLoggerOutput extends LoggerOutput {
	  write(message) {
	    // Write to file (simulation)
	    console.log(`File: ${message}`);
	  }
	}
	
	// Abstraction
	class Logger {
	  constructor(output) {
	    this.output = output;
	  }
	
	  log(message) {}
	}
	
	// Refined Abstractions
	class InfoLogger extends Logger {
	  log(message) {
	    this.output.write(`INFO: ${message}`);
	  }
	}
	
	class ErrorLogger extends Logger {
	  log(message) {
	    this.output.write(`ERROR: ${message}`);
	  }
	}
	
	// Usage
	const consoleOutput = new ConsoleLoggerOutput();
	const fileOutput = new FileLoggerOutput();
	
	const infoLogger = new InfoLogger(consoleOutput);
	const errorLogger = new ErrorLogger(fileOutput);
	
	infoLogger.log('This is an informational message');
	errorLogger.log('This is an error message');

In this example, LoggerOutput is the implementor with different concrete implementors like ConsoleLoggerOutput and FileLoggerOutput. The Logger class is the abstraction, and classes like InfoLogger and ErrorLogger are refined abstractions. This design allows you to independently vary the logger’s output and format.