简体   繁体   中英

How to correctly implement strategy design pattern

I'm trying to implement strategy design pattern, and want to know if I do it correctly.

Lets say, I have class FormBuilder which uses strategy from list below to build the form:

  • SimpleFormStrategy
  • ExtendedFormStrategy
  • CustomFormStrategy

So the questions are:

  1. Is it correct to select strategy inside FormBuilder , and not passing strategy from outside?
  2. Doesn't this violates open closed principle? So, if I want to add one more form strategy or to remove an existing one, I have to edit the FormBuilder class.

Draft code example

class Form {
    // Form data here
}

interface IFormStrategy {
    execute(params: object): Form;
}

class SimpleFormStrategy implements IFormStrategy {
    public execute(params: object): Form {
        // Here comes logics for building simple form
        return new Form();
    }
}

class ExtendedFormStrategy implements IFormStrategy {
    public execute(params: object): Form {
        // Here comes logics for building extended form
        return new Form();
    }
}

class CustomFormStrategy implements IFormStrategy {
    public execute(params: object): Form {
        // Here comes logics for building custom form
        return new Form();
    }
}

class FormBuilder {
    public build(params: object): Form {
        let strategy: IFormStrategy;

        // Here comes strategy selection logics based on params

        // If it should be simple form (based on params)
        strategy = new SimpleFormStrategy();
        // If it should be extended form (based on params)
        strategy = new ExtendedFormStrategy();
        // If it should be custom form (based on params)
        strategy = new CustomFormStrategy();

        return strategy.execute(params);
    }
}

In design pattern terms for Strategy, your FormBuilder plays the role of a Context, which holds the reference to the current strategy in use ( IFormStragegy ). The strategy is passed from outside (using setter ) so it is open to extension (OCP). So regarding your questions:

  1. Is it correct to select strategy inside FormBuilder , and not passing strategy from outside?

It is not correct implementation of strategy. You should create instances of your strategy and pass it to the context. The strategy therefore can be swapped at run-time.

  1. Doesn't this violates open closed principle? So, if I want to add one more form strategy or to remove an existing one, I have to edit the FormBuilder class.

Yes it does, you cannot make a new strategy known to the FormBuilder without changing it.

You can look here for an example.

FormBuilder context = new FormBuilder();
IFormStrategy simple = new SimpleFormStrategy();
IFormStrategy extended = new ExtendedFormStrategy();
IFormStrategy custom = new CustomFormStrategy();

context.setStrategy(simple);
context.build(/* parameters */)

context.setStrategy(custom);
context.build(/* parameters */)

You asked 2 questions that are not directly linked to TypeScript. The code can be directly converted to C# / Java, the usual mainstream OOP languages. It's even more interesting because it's about both Design Patterns and SOLID principles, both pillars of the Object Oriented Programming .

Let's answer it specifically before being more general:

  1. Is it correct to select strategy inside FormBuilder , and not passing strategy from outside?

Yes. The opposite leads to a FormFactory wrapper without much interest. Why not calling directly strategy.execute() ?

  1. Doesn't this violates open closed principle? So, if I want to add one more form strategy or to remove an existing one, I have to edit the FormBuilder class.

Builders and Factories are tightly couple to the underneath created types by design. It's a local violation of the OCP but with them the client code is decoupled from form creation implementation details.

Misc comments

  • Design patterns
    • Every design pattern must be found from the context (client code and even business domain) and not upfront. Design patterns are rarely used by the book but must be adapted to suit the context and can be mixed together.
    • The IFormStrategy is firstly an (abstract) Factory : it creates a Form . A better name should be IFormFactory { create(...): Form; } IFormFactory { create(...): Form; } (or just FormFactory , the "I" prefix being more common in C# than in TypeScript). It's a Strategy for the FormBuilder but not intrinsically. By the way, the term Strategy is rarely used when naming classes, because it's too generic. Better use a more specific/explicit term.
    • The FormBuilder is not exactly a Builder which should create an object by parts, usually with a fluent API like formBuilder.withPartA().withPartB().build(); . This class selects the appropriate Factory/Strategy based on the input params. It's a Strategy Selector , or a Factory of Factory :D that also call the factory to finally create the Form . Maybe it does too much: just selecting the factory would be enough. Maybe it's appropriate, hiding complexity from the client code.
  • OOP + Design Patterns vs Functional Programming in TypeScript
    • Strategies in a functional language are just functions. TypeScript allows to define higher order functions, with an interface / type but without a wrapping object /class that may not bring more value. The client code just have to pass another function which can be a "simple lambda" (fat arrow function).
  • Misc
    • params argument is used by both the Builder and the Factories . It would be probably better to split it, to avoid mixing distinct concerns: strategy selection and form creation .
    • If the kind of the form to create (Simple, Extended, Custom) is not dynamic but already known from the client code side, it may be better to offer directly 3 methods with specific arguments: createSimpleForm(simpleFormArgs) , createExtendedForm(extendsFormArgs) ... Each method will instanciate the associated factory and call it create(formArgs) method. This way, no need for a complex algorithm to select the strategy, based on if s or switch s which increases the Cyclomatic Complexity . Calling each createXxxForm method will also be simpler, the object argument being more little.

Strategy is a behavioral design pattern that turns a set of behaviors into objects and makes them interchangeable inside original context object.

The original object, called context, holds a reference to a strategy object and delegates it executing the behavior. In order to change the way the context performs its work, other objects may replace the currently linked strategy object with another one.

Usage examples: The Strategy pattern is very common in TypeScript code. It's often used in various frameworks to provide users a way to change the behavior of a class without extending it.

Identification: Strategy pattern can be recognized by a method that lets nested object do the actual work, as well as the setter that allows replacing that object with a different one.

Conceptual Example This example illustrates the structure of the Strategy design pattern. It focuses on answering these questions: • What classes does it consist of? • What roles do these classes play? • In what way the elements of the pattern are related?

index.ts: Conceptual Example

/**
     * The Context defines the interface of interest to clients.
     */
    class Context {
        /**
         * @type {Strategy} The Context maintains a reference to one of the Strategy
         * objects. The Context does not know the concrete class of a strategy. It
         * should work with all strategies via the Strategy interface.
         */
        private strategy: Strategy;

        /**
         * Usually, the Context accepts a strategy through the constructor, but also
         * provides a setter to change it at runtime.
         */
        constructor(strategy: Strategy) {
            this.strategy = strategy;
        }

        /**
         * Usually, the Context allows replacing a Strategy object at runtime.
         */
        public setStrategy(strategy: Strategy) {
            this.strategy = strategy;
        }

        /**
         * The Context delegates some work to the Strategy object instead of
         * implementing multiple versions of the algorithm on its own.
         */
        public doSomeBusinessLogic(): void {
            // ...

            console.log('Context: Sorting data using the strategy (not sure how it\'ll do it)');
            const result = this.strategy.doAlgorithm(['a', 'b', 'c', 'd', 'e']);
            console.log(result.join(','));

            // ...
        }
    }

    /**
     * The Strategy interface declares operations common to all supported versions
     * of some algorithm.
     *
     * The Context uses this interface to call the algorithm defined by Concrete
     * Strategies.
     */
    interface Strategy {
        doAlgorithm(data: string[]): string[];
    }

    /**
     * Concrete Strategies implement the algorithm while following the base Strategy
     * interface. The interface makes them interchangeable in the Context.
     */
    class ConcreteStrategyA implements Strategy {
        public doAlgorithm(data: string[]): string[] {
            return data.sort();
        }
    }

    class ConcreteStrategyB implements Strategy {
        public doAlgorithm(data: string[]): string[] {
            return data.reverse();
        }
    }

    /**
     * The client code picks a concrete strategy and passes it to the context. The
     * client should be aware of the differences between strategies in order to make
     * the right choice.
     */
    const context = new Context(new ConcreteStrategyA());
    console.log('Client: Strategy is set to normal sorting.');
    context.doSomeBusinessLogic();

    console.log('');

    console.log('Client: Strategy is set to reverse sorting.');
    context.setStrategy(new ConcreteStrategyB());
    context.doSomeBusinessLogic();

Output.txt: Execution result

Client: Strategy is set to normal sorting.
Context: Sorting data using the strategy (not sure how it'll do it)
a,b,c,d,e

Client: Strategy is set to reverse sorting.
Context: Sorting data using the strategy (not sure how it'll do it)
e,d,c,b,a

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM