The Strategy Pattern describes a way of removing a hardcoded behaviour from a class, and instead offloads that behaviour to a separate interface. Or, to quote the book:

Define a family of algorithms, encapsulate each one, and make them interchangeable. Strategy lets the algorithm vary independently from clients that use it.

Instinctively, you might reach for a subclass when wanting to define different behaviour in a class. Let’s suspend belief for a minute and pretend there is a reason you'd need a create a class to hash a value. I’m fond of TypeScript recently so let’s use that for the code samples.

abstract class Hasher {
  public abstract hash(value: string): string
}

class Md5Hasher extends Hasher {
  public hash(value: string): string {
    md5(value)
  }
}

class Sha256Hasher extends Hasher {
  public hash(value: string): string {
    sha256(value)
  }
}

const hasher = new Sha256Hasher()
hasher.hash("value to hash")

This is fine, but would quickly grow unwieldy when implementing different hashing functions.

Suppose, more realistically, you want the hasher to output the value somewhere, say the console, or a file:

abstract class HasherAndOutputter {
  protected abstract hash(value: string): string

  protected abstract output(value: string): void

  public run(value: string): void {
    const hashedValue = this.hash(value)
    this.output(hashedValue)
  }
}

class Md5ConsoleLogOutputter extends HasherAndOutputter {
  // ...
}

class Md5FileOutputter extends HasherAndOutputter {
  // ...
}

class Sha256ConsoleLogOutputter extends HasherAndOutputter {
  // ...
}

class Sha256FileOutputter extends HasherAndOutputter {
  // ...
}

As you can see, this really doesn't scale well! We've created subclasses that contain concrete implementations of both the behaviour to hash, and the behaviour to output. This means that the behaviour cannot easily be swapped out, or reused.

Let's instead take a look at what the Strategy Pattern might look like when applied to this scenario:

interface Hasher {
    hash(value: string): string;
}

class Md5Hasher implements Hasher {
    // ...
}

class Sha256Hasher implements Hasher {
    // ...
}

interface Outputter {
    output(value: string): void;
}

class ConsoleLogOutputter implements Outputter {
    // ...
}

class FileOutputter implements Outputter {
    // ...
}

class HasherAndOutputter {
    private hasher: Hasher;
    private outputter: Outputter;

    constructor(hasher: Hasher, outputter: Outputter) {
        this.hasher = hasher;
        this.outputter = outputter;
    }

    public run(value: string) {
        const hashedValue = this.hasher.hash(value);

        this.outputter.output(hashedValue);
    }
}

const md5AndConsoleLog = new HasherAndOutputter(new Md5Hasher(), new ConsoleLogOutputter());
const Sha256AndConsoleLog = // ...
const md5AndFile = // ...
const Sha256AndFile = //...

As you can see, rather than creating a subclass of the HasherAndOutputter for each scenario, we move the hashing and outputting behaviour into their own classes, and define a property for each on the HasherAndOutputter class.

These loosely coupled dependencies make it easy to swap out implementations without changing the underlying code. You might even say that the behaviour is being composed (favour composition over inheritance).

Isn't This Just Dependency Injection (DI)?

Yes and no. We are injecting dependencies into the class as it is created, but that doesn’t necessarily need to be the case. The Strategy Pattern is less about injecting the behaviours and more about removing the behaviours from the class entirely: the point isn't that they are now injectable, the point is they now exist outside of the class.

For instance, we can easily define a method to set the hashing behaviour at a later date:

class HasherAndOutputter {
  // ...
  public setHasher(hasher: Hasher): void {
    this.hasher = hasher
  }
}

const hashAndOutput = new HasherAndOutputter(
  new Md5Hasher(),
  new ConsoleLogOutputter()
)
hashAndOutput.run("hash")

// ... later

hashAndOutput.setHasher(new Sha256Hasher())
hashAndOutput.run("hash")

Or, more radically, we might define all possible behaviours inside a class, and determine which one to run based on some input:

interface ValidationCheck {
  check(input: string): boolean
}

class DateOfBirthValidator implements ValidationCheck {
  // ...
}

class UsernameValidator implements ValidationCheck {
  // ...
}

class Validator {
  // Here, we are hardcoding the possible ValidationChecks that the
  // Validator class can make use of.
  private validators: { [inputType: string]: ValidationCheck } = {
    dob: new DateOfBirthValidator(),
    username: new UsernameValidator(),
  }

  check(input: string, inputType: string): boolean {
    // If a ValidationCheck exists for the given inputType,
    // then run it with the given input.
    if (this.validators[inputType]) {
      return this.validators[inputType].check(input)
    }

    return false
  }
}

const validator = new Validator()

console.log(validator.check("zeal", "username"))

This example maybe makes more sense, or certainly makes things more obvious: you'd never think about creating a Validator class that has all this behaviour concretely defined inside it, would you?

Another advantage of moving this behaviour into its own class, is that we can then use that class wherever we want. For instance, given the validator above, we could just call the check method directly on the username class, if all we want to check is the username:

const uv = new UsernameValidator()

uv.check("zeal")

A Real-world Example

I took this pattern and used it in a realistic scenario to define a caching behaviour
for a Pokemon API, you take take a look at the example code on GitHub.