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
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).
// 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.