Skip to content

Strategy

Structure

Role Example Responsibility
Context SortingContext Maintains a reference to a Strategy and delegates execution to it
Strategy SortingStrategy The interface defining the contract all concrete strategies must fulfil
Concrete Strategy BubbleSort, QuickSort, MergeSort Implements a specific algorithm variant

Steps

  1. Create the Strategy interface
  2. Implement Concrete Strategies
  3. Create the Context class
  4. Inject or change the strategy at runtime
classDiagram
    class SortingStrategy {
        <<interface>>
        +sort(data): array
        +validate(data): bool
        +getComplexity(): string
    }

    class BubbleSort {
        +sort(data): array
        +validate(data): bool
        +getComplexity(): string
    }

    class QuickSort {
        +sort(data): array
        +validate(data): bool
        +getComplexity(): string
    }

    class MergeSort {
        +sort(data): array
        +validate(data): bool
        +getComplexity(): string
    }

    class SortingContext {
        -strategy: SortingStrategy
        +__construct(SortingStrategy)
        +setStrategy(SortingStrategy)
        +applySort(elements): array
        +describeStrategy(): string
    }

    BubbleSort ..|> SortingStrategy
    QuickSort ..|> SortingStrategy
    MergeSort ..|> SortingStrategy
    SortingContext --> SortingStrategy : delegates

The context (like SortingContext) delegates the work to whichever strategy (like BubbleSort, QuickSort, MergeSort) is injected, allowing the algorithm to be swapped at runtime without modifying the context.


Example: Sorting Algorithms

Strategy Interface

SortingStrategy.php
<?php

declare(strict_types=1);

namespace DesignPattern\Behavioural\Strategy\Sorting\Strategy;

interface SortingStrategy
{
    /**
     * @param array<int, int> $data
     * @return array<int, int>
     */
    public function sort(array $data): array;

    /**
     * @param array<int, int> $data
     * @return bool
     */
    public function validate(array $data): bool;

    /**
     * @return string
     */
    public function getComplexity(): string;
}

Context

SortingContext.php
<?php

declare(strict_types=1);

namespace DesignPattern\Behavioural\Strategy\Sorting;

use DesignPattern\Behavioural\Strategy\Sorting\Strategy\SortingStrategy;

class SortingContext
{
    /**
     * @param SortingStrategy $strategy
     */
    public function __construct(
        private SortingStrategy $strategy,
    ) {
    }

    /**
     * @param array<int, int> $elements
     * @return array<int, int>
     */
    public function applySort(array $elements): array
    {
        if (!$this->strategy->validate($elements)) {
            throw new \InvalidArgumentException("Data failed strategy validation.");
        }
        return $this->strategy->sort($elements);
    }

    /**
     * @return string
     */
    public function describeStrategy(): string
    {
        return $this->strategy->getComplexity();
    }

    /**
     * @param SortingStrategy $strategy
     * @return void
     */
    public function setStrategy(SortingStrategy $strategy): void
    {
        $this->strategy = $strategy;
    }
}

Strategies

BubbleSort.php
<?php

declare(strict_types=1);

namespace DesignPattern\Behavioural\Strategy\Sorting\Strategy;

class BubbleSort implements SortingStrategy
{
    /**
     * @param array<int, int> $data
     * @return array<int, int>
     */
    public function sort(array $data): array
    {
        echo "Sorting using Bubble Sort\n";
        return $data;
    }

    /**
     * @param array<int, int> $data
     * @return bool
     */
    public function validate(array $data): bool
    {
        return count($data) > 1;
    }

    public function getComplexity(): string
    {
        return "Average: O(n²) | Worst case: O(n²)\n";
    }
}
QuickSort.php
<?php

declare(strict_types=1);

namespace DesignPattern\Behavioural\Strategy\Sorting\Strategy;

class QuickSort implements SortingStrategy
{
    /**
     * @param array<int, int> $data
     * @return array<int, int>
     */
    public function sort(array $data): array
    {
        echo "Sorting using Quick Sort\n";
        return $data;
    }

    /**
     * @param array<int, int> $data
     * @return bool
     */
    public function validate(array $data): bool
    {
        return count($data) > 1;
    }

    public function getComplexity(): string
    {
        return "Average: O(n log n) | Worst case: O(n²)\n";
    }
}
MergeSort.php
<?php

declare(strict_types=1);

namespace DesignPattern\Behavioural\Strategy\Sorting\Strategy;

class MergeSort implements SortingStrategy
{
    /**
     * @param array<int, int> $data
     * @return array<int, int>
     */
    public function sort(array $data): array
    {
        echo "Sorting using Merge Sort\n";
        return $data;
    }

    /**
     * @param array<int, int> $data
     * @return bool
     */
    public function validate(array $data): bool
    {
        return count($data) > 1;
    }

    public function getComplexity(): string
    {
        return "Average: O(n log n) | Worst case: O(n log n)\n";
    }
}

Tests

SortingTest.php
<?php

declare(strict_types=1);

namespace Tests\Behavioural\Strategy\Sorting;

use DesignPattern\Behavioural\Strategy\Sorting\SortingContext;
use DesignPattern\Behavioural\Strategy\Sorting\Strategy\BubbleSort;
use DesignPattern\Behavioural\Strategy\Sorting\Strategy\QuickSort;
use DesignPattern\Behavioural\Strategy\Sorting\Strategy\MergeSort;
use DesignPattern\Behavioural\Strategy\Sorting\Strategy\SortingStrategy;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;

#[CoversClass(SortingContext::class)]
class SortingTest extends TestCase
{
    /**
     * @param class-string<SortingStrategy> $strategyClass
     */
    #[DataProvider('strategyProvider')]
    public function testAllStrategies(string $strategyClass): void
    {
        $data    = [9, 1, 7, 3, 5];
        $context = new SortingContext(new $strategyClass());

        ob_start();
        $result = $context->applySort($data);
        ob_end_clean();

        self::assertSame($data, $result);
    }

    /**
     * @return array<string, array<int, string>>
     */
    public static function strategyProvider(): array
    {
        return [
            'QuickSort'  => [QuickSort::class],
            'BubbleSort' => [BubbleSort::class],
            'MergeSort'  => [MergeSort::class],
        ];
    }

    public function testQuickSortOutput(): void
    {
        $context = new SortingContext(new QuickSort());

        ob_start();
        $context->applySort([1, 2, 3]);
        $output = ob_get_clean();

        self::assertSame("Sorting using Quick Sort\n", $output);
    }

    public function testBubbleSortOutput(): void
    {
        $context = new SortingContext(new BubbleSort());

        ob_start();
        $context->applySort([1, 2, 3]);
        $output = ob_get_clean();

        self::assertSame("Sorting using Bubble Sort\n", $output);
    }

    public function testMergeSortOutput(): void
    {
        $context = new SortingContext(new MergeSort());

        ob_start();
        $context->applySort([1, 2, 3]);
        $output = ob_get_clean();

        self::assertSame("Sorting using Merge Sort\n", $output);
    }

    public function testQuickSortComplexity(): void
    {
        $context = new SortingContext(new QuickSort());
        self::assertSame("Average: O(n log n) | Worst case: O(n²)\n", $context->describeStrategy());
    }

    public function testBubbleSortComplexity(): void
    {
        $context = new SortingContext(new BubbleSort());
        self::assertSame("Average: O(n²) | Worst case: O(n²)\n", $context->describeStrategy());
    }

    public function testMergeSortComplexity(): void
    {
        $context = new SortingContext(new MergeSort());
        self::assertSame("Average: O(n log n) | Worst case: O(n log n)\n", $context->describeStrategy());
    }

    /**
     * @param class-string<SortingStrategy> $strategyClass
     */
    #[DataProvider('strategyProvider')]
    public function testValidationRejectsEmptyArray(string $strategyClass): void
    {
        $context = new SortingContext(new $strategyClass());

        $this->expectException(\InvalidArgumentException::class);
        $this->expectExceptionMessage('Data failed strategy validation.');

        $context->applySort([]);
    }

    public function testSetStrategyChangesComplexityDescription(): void
    {
        $context = new SortingContext(new QuickSort());
        self::assertStringContainsString('O(n log n)', $context->describeStrategy());

        $context->setStrategy(new BubbleSort());
        self::assertStringContainsString('O(n²)', $context->describeStrategy());

        $context->setStrategy(new MergeSort());
        self::assertStringContainsString('O(n log n)', $context->describeStrategy());
    }

    public function testSetStrategyChangesOutputMessage(): void
    {
        $context = new SortingContext(new QuickSort());

        ob_start();
        $context->applySort([1, 2, 3]);
        self::assertSame("Sorting using Quick Sort\n", ob_get_clean());

        $context->setStrategy(new MergeSort());

        ob_start();
        $context->applySort([1, 2, 3]);
        self::assertSame("Sorting using Merge Sort\n", ob_get_clean());
    }
}