Structural patterns deal with how classes and objects are composed into larger structures.
Adapter converts the interface of one class into an interface a client expects, letting otherwise-incompatible classes work together — much like a physical power plug adapter lets a device with one plug shape work with an outlet of a different shape.
// An existing, third-party class with an interface you can't change
class OldPaymentSystem {
function makePayment(amountInCents) { ... }
}
// The interface your application actually expects
interface PaymentGateway {
function pay(amountInDollars)
}
// The adapter bridges the gap
class PaymentAdapter implements PaymentGateway {
function __construct(private OldPaymentSystem oldSystem) {}
function pay(amountInDollars) {
this.oldSystem.makePayment(amountInDollars * 100) // converts dollars to cents
}
}
gateway = new PaymentAdapter(new OldPaymentSystem())
gateway.pay(19.99) // application code only ever talks to the PaymentGateway interface
Adapters are especially common when integrating third-party libraries or legacy code whose interface doesn't match what the rest of your application expects, without needing to modify that external code directly.
Decorator adds new behavior to an individual object dynamically, by wrapping it in another object that implements the same interface, without modifying the original class or affecting other instances of it.
interface Coffee {
function cost(): number
function description(): string
}
class SimpleCoffee implements Coffee {
function cost() { return 2.00 }
function description() { return "Coffee" }
}
class MilkDecorator implements Coffee {
function __construct(private Coffee coffee) {}
function cost() { return this.coffee.cost() + 0.50 }
function description() { return this.coffee.description() + " + Milk" }
}
class SugarDecorator implements Coffee {
function __construct(private Coffee coffee) {}
function cost() { return this.coffee.cost() + 0.25 }
function description() { return this.coffee.description() + " + Sugar" }
}
order = new SugarDecorator(new MilkDecorator(new SimpleCoffee()))
print(order.description()) // "Coffee + Milk + Sugar"
print(order.cost()) // 2.75
Each decorator wraps the previous object and adds its own contribution, while still exposing the exact same Coffee interface — so decorators can be stacked in any combination, and calling code never needs to know how many layers of decoration are present. This is a flexible alternative to creating a separate subclass for every possible combination of add-ons (CoffeeWithMilkAndSugar, CoffeeWithMilkOnly, etc.), which would explode combinatorially.