Decorator Pattern

To dynamically add new functionality to class instances.


Purpose

To dynamically add new functionality to class instances.

UML


Code

Booking


namespace DesignPatterns\Structural\Decorator;

interface Booking
{
    public function calculatePrice(): int;
    public function getDescription(): string;
}

DoubleRoomBooking


namespace DesignPatterns\Structural\Decorator;

class DoubleRoomBooking implements Booking
{
    public function calculatePrice(): int
    {
        return 40;
    }

    public function getDescription(): string
    {
        return 'double room';
    }
}

BookingDecorator


namespace DesignPatterns\Structural\Decorator;

abstract class BookingDecorator implements Booking
{
    public function __construct(protected Booking $booking)
    {

    }
}

ExtraBed


namespace DesignPatterns\Structural\Decorator;

class ExtraBed extends BookingDecorator
{
    private const PRICE = 30;

    public function calculatePrice(): int
    {
        return $this->booking->calculatePrice() + self::PRICE;
    }

    public function getDescription(): string
    {
        return $this->booking->getDescription() . ' with extra bed';
    }
}

WiFi


namespace DesignPatterns\Structural\Decorator;

class WiFi extends BookingDecorator
{
    private const PRICE = 2;

    public function calculatePrice(): int
    {
        return $this->booking->calculatePrice() + self::PRICE;
    }

    public function getDescription(): string
    {
        return $this->booking->getDescription() . ' with wifi';
    }
}

Tests


// Customer wants a double room. Price is £40 and the description is ‘double room’.
public function testCanCalculatePriceForBasicDoubleRoomBooking()
{
    $booking = new DoubleRoomBooking();

    $this->assertSame(40, $booking->calculatePrice());
    $this->assertSame('double room', $booking->getDescription());
}

// Customer wants a double room with Wifi. Price is now £42 with the description ‘double room with wifi’.
public function testCanCalculatePriceForDoubleRoomBookingWithWiFi()
{
    $booking = new DoubleRoomBooking();
    $booking = new WiFi($booking);

    $this->assertSame(42, $booking->calculatePrice());
    $this->assertSame('double room with wifi', $booking->getDescription());
}

// Customer wants a double room with Wifi and an extra bed. Price is now £72 with the description ‘double room with wifi with extra bed’.
public function testCanCalculatePriceForDoubleRoomBookingWithWiFiAndExtraBed()
{
    $booking = new DoubleRoomBooking();
    $booking = new WiFi($booking);
    $booking = new ExtraBed($booking);

    $this->assertSame(72, $booking->calculatePrice());
    $this->assertSame('double room with wifi with extra bed', $booking->getDescription());
}