A step-by-step guide on effectively adopting the strategy design pattern

Seeking guidance on the implementation of the strategy design pattern to ensure correctness.

Consider a scenario where the class FormBuilder employs strategies from the following list in order to construct the form:

  • SimpleFormStrategy
  • ExtendedFormStrategy
  • CustomFormStrategy

The questions at hand are:

  1. Is it appropriate to select the strategy within the FormBuilder instead of passing it from an external source?
  2. Does this approach potentially violate the open-closed principle? Meaning, if a new form strategy needs to be added or an existing one removed, would it require modifications in the FormBuilder class?

Draft code snippet provided below:

class Form {
    // Form data here
}

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

class SimpleFormStrategy implements IFormStrategy {
    public execute(params: object): Form {
        // Logic for building simple form goes here
        return new Form();
    }
}

class ExtendedFormStrategy implements IFormStrategy {
    public execute(params: object): Form {
        // Logic for building extended form goes here
        return new Form();
    }
}

class CustomFormStrategy implements IFormStrategy {
    public execute(params: object): Form {
        // Logic for building custom form goes here
        return new Form();
    }
}

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

        // Strategy selection logic based on params goes here

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

        return strategy.execute(params);
    }
}

Answer №1

Two questions were raised that are not directly related to TypeScript, but can be easily converted to C# or Java - the mainstream OOP languages. These questions touch on Design Patterns and SOLID principles, which are key aspects of Object Oriented Programming.

Let's address these questions specifically before delving into more general concepts:

  1. Is it appropriate to choose a strategy within the FormBuilder rather than passing the strategy from an external source?

Yes, opting for this approach eliminates the need for a redundant FormFactory wrapper. It is more efficient to directly call strategy.execute().

  1. Doesn't this violate the open-closed principle? If additional form strategies need to be added or existing ones removed, does this mean editing the FormBuilder class?

Builders and Factories are inherently linked to the types they create. While there may be a local violation of the Open Closed Principle (OCP), using them ensures that client code remains decoupled from the specifics of form creation implementation.

Miscellaneous Insights

  • Design patterns
    • Each design pattern should be derived based on context (client code or business domain) rather than adopted upfront. Adapting design patterns to suit the specific context and potentially combining them is common practice.
    • The IFormStrategy primarily acts as a (abstract) Factory, creating a Form. A more suitable name could be
      IFormFactory { create(...): Form; }
      (or simply FormFactory, as the "I" prefix is more prevalent in C# compared to TypeScript). While it functions as a Strategy for the FormBuilder, it isn't inherently intrinsic to it. Furthermore, the term Strategy is infrequently used when naming classes due to its generic nature. A more specific or explicit terminology is often preferred.
    • The FormBuilder doesn't precisely act as a Builder, which typically constructs objects in parts through a fluent API (e.g.,
      formBuilder.withPartA().withPartB().build();
      ). Instead, it chooses the appropriate Factory/Strategy based on input parameters. It might be considered a Strategy Selector or even a Factory of Factory :D, as it also invokes the factory to ultimately generate the Form. It could potentially be handling too many responsibilities - simply selecting the factory could suffice. However, it might be justified if it simplifies complexity for client code.
  • OOP + Design Patterns vs Functional Programming in TypeScript
    • In a functional language, Strategies equate to simple functions. In TypeScript, defining higher-order functions with an interface/type suffices without necessitating a wrapping object/class. The client code merely needs to supply another function, which can be a straightforward lambda (fat arrow function).
  • Miscellaneous
    • The params argument is utilized by both the Builder and the Factories. Suggestively, segregating it could prevent mingling distinct concerns: strategy selection versus form creation.
    • If the form type (Simple, Extended, Custom) isn't dynamic but predetermined from the client code end, offering three distinct methods with specific arguments (e.g., createSimpleForm(simpleFormArgs),
      createExtendedForm(extendsFormArgs)
      ) might be a cleaner alternative. Each method would instantiate the corresponding factory and invoke its create(formArgs) method. This approach eliminates the need for intricate strategy selection algorithms based on conditionals (if/switch), thus reducing Cyclomatic Complexity. Calling each createXxxForm method becomes simpler since the object argument is minimal.

Answer №2

When it comes to design patterns and the Strategy pattern, think of your FormBuilder as the Context, holding a reference to the current strategy being used (IFormStrategy). This strategy is provided from external sources (using a setter), allowing for extension and following the Open/Closed Principle.

  1. Is it appropriate to select the strategy inside the FormBuilder rather than passing it from outside?

Selecting the strategy within the FormBuilder is not an ideal strategy implementation. Instead, create instances of the strategy and pass them to the context. This allows for runtime swapping of strategies.

  1. Doesn't this violate the Open/Closed Principle? If I need to add or remove a form strategy, do I have to modify the FormBuilder class?

Indeed, this approach does violate the Open/Closed Principle. To introduce a new strategy to the FormBuilder, you would need to make changes directly to the class.

For an example, refer here.

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 */) 

Answer №3

Strategy is a design pattern that transforms behaviors into objects, allowing them to be interchangeable within the original context object.

The main object, known as the context, holds a reference to a strategy object and delegates the execution of the behavior to it. By swapping the currently linked strategy object with another one, the way in which the context performs its work can be altered.

Examples of Usage: The Strategy pattern is frequently utilized in TypeScript code, especially in frameworks where users need to adjust the behavior of a class without extending it.

Identification: The Strategy pattern can be recognized by a method that enables nested objects to carry out the actual work, along with a setter that allows for the substitution of that object with a different one.

Conceptual Example This example showcases the structure of the Strategy design pattern and addresses questions such as: • What are the classes involved? • What roles do these classes fulfill? • How are the elements of the pattern connected?

index.ts: Conceptual Example

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

        /**
         * Typically, the Context receives a strategy through the constructor but also provides a setter to switch it during runtime.
         */
        constructor(strategy: Strategy) {
            this.strategy = strategy;
        }

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

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

            console.log('Context: Sorting data using the strategy (uncertain about the process)');
            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 a particular algorithm.
     *
     * The Context uses this interface to invoke the algorithm specified by Concrete
     * Strategies.
     */
    interface Strategy {
        doAlgorithm(data: string[]): string[];
    }

    /**
     * Concrete Strategies implement the algorithm following the base Strategy
     * interface, making 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 selects a specific strategy and passes it to the context. Being aware of the distinctions between strategies is crucial for making an informed choice.
     */
    const context = new Context(new ConcreteStrategyA());
    console.log('Client: Strategy set for normal sorting.');
    context.doSomeBusinessLogic();

    console.log('');

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

Output.txt: Execution outcome

Client: Strategy set for normal sorting.
Context: Sorting data using the strategy (uncertain about the process)
a,b,c,d,e

Client: Strategy set for reverse sorting.
Context: Sorting data using the strategy (uncertain about the process)
e,d,c,b,a

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

The Axios and TypeScript promise rejection error is displaying an unknown type- cannot identify

Currently, I am encountering an issue where I am unable to utilize a returned error from a promise rejection due to its lack of typability with Typescript. For instance, in the scenario where a signup request fails because the username is already taken, I ...

Filling a data entry with simultaneous commitments

Sample code: type Alphabet = 'a' | 'b' | 'c'; const alphabetMap: Record<Alphabet, null> = { 'a': null, 'b': null, 'c': null} // Select any asynchronous processing function you prefer funct ...

RxJS pipe operation ignoring observable

Currently, I am in the process of transitioning an app from Promises to RxJS and I could use some guidance on whether I am heading in the right direction. Situation: I have a ModalComponent that appears when an HTTP request is sent and disappears once the ...

Can the type of a prop be specified in the parent component before it is passed down to the children components that utilize the same prop?

In my codebase, I have a component called NotFoundGuard. This component only renders its children if a certain condition is true. Otherwise, it displays a generic fallback component to prevent redundancy in our code. I am trying to figure out if there is ...

"Encountering a module not found issue while trying to

Attempting to test out 3 node modules locally by updating their source locations in the package.json files. The modules in question are sdk, ng-widget-lib, and frontend. ng-widget-lib relies on sdk, while frontend depends on ng-widget-lib. To locally build ...

Using TypeScript, a parameter is required only if another parameter is passed, and this rule applies multiple

I'm working on a concept of a distributed union type where passing one key makes other keys required. interface BaseArgs { title: string } interface FuncPagerArgs { enablePager: true limit: number count: number } type FuncArgs = (Fu ...

Could this type declaration in the Vue decorator constructor be accurate?

When using Vue decorator notation, I typically write it like this: @Prop({ type: Object || null, default: null }) However, I noticed in the Vue documentation that they use array notation: @Prop({ type: [ Object, null ], default: null }) Is there a specif ...

Create a collection of values and assign it to a form control in Ionic 2

How can I set default values for ion-select with multiple choices using a reactive form in Angular? FormA:FormGroup; this.FormA = this.formBuilder.group({ toppings:['',validators.required] }); <form [formGroup]="FormA"> <i ...

What is the process for importing types from the `material-ui` library?

I am currently developing a react application using material-ui with typescript. I'm on the lookout for all the type definitions for the material component. Despite attempting to install @types/material-ui, I haven't had much success. Take a look ...

Having issues with Angular material autocomplete feature - not functioning as expected, and no error

I have set up my autocomplete feature, and there are no error messages. However, when I type something in the input field, nothing happens - it seems like there is no action being triggered, and nothing appears in the console. Here is the HTML code: ...

Experiencing "localhost redirect loop due to NextJS Middleware" error

After successfully integrating email/password authentication to my locally hosted NextJS app using NextAuth, I encountered an issue with the middleware I created to secure routes. Every time I tried to sign out, I received an error stating "localhost redir ...

Tips for saving metadata about properties within a class

I am looking to add metadata to properties within classes, specifically using abbreviations for property names. By using annotations like @shortName(abbreviated), you can label each property as follows: function shortName(shortName: string){ return fu ...

Is it necessary to 'type assert' the retrieved data in Axios if I have already specified the return type in the function declaration?

Consider the code snippet below: import axios from 'axios' async function fetchAPI<T>(path: string, data: any): Promise<T> { return (await axios.get(path, data)).data as T } async function getSomething(): Promise<SomeType> { ...

"Enable email delivery in the background on a mobile app without relying on a server

I am currently in the process of developing a mobile app using Ionic. One feature I would like to incorporate is sending an email notification to admins whenever a post is reported within the app. However, I am facing challenges with implementing this succ ...

Integrate a fresh global JSX attribute into your React project without the need for @types in node_modules

It is crucial not to mention anything in tsconfig.json. Error Type '{ test: string; }' cannot be assigned to type 'DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement>'. Property 'test' does not exi ...

Seamless database migrations using sequelize and typescript

I've been exploring the concept of generating migration files for models that already exist. When I use the "force: true" mode, tables are automatically created in the database, so I find it hard to believe that creating migration files automatically ...

A step-by-step guide on incorporating MarkerClusterer into a google-map-react component

I am looking to integrate MarkerClusterer into my Google Map using a library or component. Here is a snippet of my current code. Can anyone provide guidance on how I can achieve this with the google-map-react library? Thank you. const handleApiLoaded = ({ ...

Retrieve data from an HTML form within an Angular 2 ag-grid component

I'm facing a challenge with incorporating form values from a modal into an ag-grid array in my HTML file. I'm unsure of the process to achieve this. Below is an excerpt from my file.html: <template #addTrainContent let-c="close" let-d="dismi ...

Changes in the styles of one component can impact the appearance of other

When it comes to styling my login page, I have specific stylesheets that I include in login.component.ts. For all the common CSS files, I have added them in the root index ("index.html") using the traditional method. However, after a user logs into the sys ...