Split an abstraction from its implementation so each can change independently, instead of multiplying subclasses.
A `Shape` that can be `Circle` or `Square`, rendered by either `RasterRenderer` or `VectorRenderer`, tempts you into `RasterCircle`, `VectorCircle`, `RasterSquare`, `VectorSquare`. Add a third shape or renderer and the subclass count multiplies again.
The abstraction (Shape) holds a reference to an implementation (Renderer) rather than inheriting from it. Either hierarchy can grow — new shapes or new renderers — without touching the other.
Any shape can be paired with any renderer at construction time — the pairing is a runtime choice, not a class.
const RasterRenderer = { render: (desc) => console.log(`[raster] drawing ${desc}`) };
const VectorRenderer = { render: (desc) => console.log(`[vector] drawing ${desc}`) };
class Shape {
constructor(renderer) { this.renderer = renderer; }
}
class Circle extends Shape {
draw() { this.renderer.render("circle"); }
}
class Square extends Shape {
draw() { this.renderer.render("square"); }
}
new Circle(RasterRenderer).draw(); // [raster] drawing circle
new Circle(VectorRenderer).draw(); // [vector] drawing circle
new Square(VectorRenderer).draw(); // [vector] drawing square