DEV Community

loading...
Cover image for πŸ“— Object Behavioural: Strategy

πŸ“— Object Behavioural: Strategy

Jokerwolf
"I know that i know nothing". Learning stuff. Writing stuff down helps me remember it better. Being able to share with others definitely won't hurt too.
・3 min read

TL;DR;

Use this pattern if you have multiple similar classes, which differ in some logic implementation; your code contains if-else's to determine a proper algorithm to use.
Second pattern in a row targets to eliminate if-else's where possible - nice.

Definition

Strategy pattern allows separating algorithms from the clients using them.

Here is a formal diagram:
UML diagram of Strategy pattern

We have an object of type Context, which holds a reference to an IStrategy object. Context passes some work to this IStrategy object.

Sounds pretty much the same as State Pattern, but the strategy is set once at the Context instantiating time (while state is changing after Context is instantiated).

Implementation

Let's write some code.
We still have a Player, but this time we're more interested in its codecs.
So we would end up with something like this:

export default class Player {
  private paramA: number;
  private paramB: number;
  private paramC: number;

  constructor(private codecType: AlgorithmType, private stream: Stream) {
    // Setting up params
  }

  play() {
    const algorithm = this.getAlgorithm();

    switch (algorithm) {
      case 'A':
        this.decodeA();
        break;
      case 'B':
        this.decodeB();
        break;
      case 'C':
        this.decodeC();
        break;
    }

    console.log('>>>> Play');
  }

  private getAlgorithm(): AlgorithmType {
    // Some logic happens here to determine appropriate algorithm.
    return this.codecType;
  }

  private decodeA(): Stream {
    // Actual algorithm to decode the stream in some way
    console.log('>>>> Decoding with A', this.paramA);
    return this.stream;
  }

  private decodeB(): Stream {
    // Actual algorithm to decode the stream in some way
    console.log('>>>> Decoding with B', this.paramB);
    return this.stream;
  }

  private decodeC(): Stream {
    // Actual algorithm to decode the stream in some way
    console.log('>>>> Decoding with C', this.paramC);
    return this.stream;
  }
}
Enter fullscreen mode Exit fullscreen mode

We have all different ways of decoding a stream inside the Player class. We have a switch statement to determine which particular algorithm we should use. Not that bad right now, but imagine a real life situation where decode functions could contain tens of lines of code.

What Strategy suggests is:

  • create an interface to describe the strategy class
  • move each decoding algorithm into a standalone strategy class
  • add a reference to the particular instance to you Player

Well, let's do all these with one small change: let's not create a class for each algorithm. Look at this:

interface Strategy {
  decode(stream: Stream, context: DecodeContext): Stream;
}

class StrategyA implements Strategy {
  decode(stream: Stream, context: DecodeContext) {
    // implementation
  }
}
Enter fullscreen mode Exit fullscreen mode

We're in the TypeScript (JavaScript) world, functions are the first class citizens, we can pass them everywhere, we don't need to create a class just to have one function in it.

So this is what our pattern implementation might look like:

export default class Player {
  private paramA: number;
  private paramB: number;
  private paramC: number;

  constructor(private decode: Decode, private stream: Stream) {
    // Setting up params
  }

  play() {
    this.decode(this.stream, {
      a: this.paramA,
      b: this.paramB,
      c: this.paramC,
    });
    console.log('>>>> Play');
  }
}
Enter fullscreen mode Exit fullscreen mode

And all decoding algorithms now live in their own functions:

export const decodeA: Decode = (stream: Stream, { a }: DecodeContext) => {
  // Actual algorithm to decode the stream in some way
  console.log('>>>> Decoding with A', a);
  return stream;
};

export const decodeB: Decode = (stream: Stream, { b }: DecodeContext) => {
  // Actual algorithm to decode the stream in some way
  console.log('>>>> Decoding with B', b);
  return stream;
};

export const decodeC: Decode = (stream: Stream, { c }: DecodeContext) => {
  // Actual algorithm to decode the stream in some way
  console.log('>>>> Decoding with C', c);
  return stream;
};
Enter fullscreen mode Exit fullscreen mode

What will it cost?

Potential Cons:

  • We might increase the number of files, which some might consider a bad thing
  • As we define the Decode interface for every algorithm we're passing all the params each algorithm might need. Less then ideal

Potential Pros:

  • We got rid of the switch statement
  • We separated concerns by moving algorithms implementations outside the Player class

Conclusion

I do like separating concerns and avoiding conditionals when possible, so this pattern seems really useful to me so far.

Hope this was a bit helpful πŸ™ƒ
Source code if needed

Discussion (0)