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

How to specify in TypeScript that if one key is present, another key must also be present, without redundantly reproducing the entire structure

In my code, I have a custom type defined like this (but it's not working): type MyType = | { foo: string; } | { foo: string; barPre: string; barPost: string; } | { foo: string; quxPre: string; qu ...

I am experiencing an issue with applying responsiveFontSize() to the new variants in Material UI Typography

I am looking to enhance the subtitles in MUI Typography by adding new variants using Typescript, as outlined in the documentation here. I have defined these new variants in a file named global.d.ts, alongside other customizations: // global.d.ts import * a ...

Unable to connect to web3 object using typescript and ethereum

Embarking on a fresh project with Angular 2 and TypeScript, I kicked things off by using the command: ng new myProject Next, I integrated web3 (for Ethereum) into the project through: npm install web3 To ensure proper integration, I included the follow ...

Why does mapping only give me the last item when I try to map onto an object?

Why does mapping onto an object only give me the last item? Below is the object displayed in the console: 0: {Transport: 2} 1: {Implementation: 9} 2: {Management: 3} When I use ngFor, it only provides the last item const obj = this.assigned_group; // r ...

Issue with default country placeholder in Ionic 6.20.1 when using ion-intl-tel-input

I have successfully downloaded and set up the "ion-intl-tel-input" plugin from https://github.com/azzamasghar1/ion-intl-tel-input, and it is functioning properly. However, I am facing an issue with changing the default country select box placeholder from " ...

The image map library functions seamlessly with React but encounters issues when integrated with Next.js

I have been working on a client project that requires the use of an image map. I searched for a suitable library, but struggled to find one that is easy to maintain. However, I came across this particular library that seemed promising. https://github.com/ ...

Analyzing past UTC date times results in a peculiar shift in time zones

When I receive various times in UTC from a REST application, I encounter different results. Examples include 2999-01-30T23:00:00.000Z and 1699-12-30T23:00:00.000Z. To display these times on the front end, I use new Date(date) in JavaScript to convert the ...

Tips for maintaining an open ng-multiselect-dropdown at all times

https://www.npmjs.com/package/ng-multiselect-dropdown I have integrated the ng multiselect dropdown in my Angular project and I am facing an issue where I want to keep it always open. I attempted using the defaultOpen option but it closes automatically. ...

Tips for patiently anticipating the outcome of asynchronous procedures?

I have the given code snippet: async function seedDb() { let users: Array<Users> = [ ... ]; applications.map(async (user) => await prisma.user.upsert( { create: user, update: {}, where: { id: user.id } })); } async function main() { aw ...

Ways to effectively test public functions in Typescript when using react-testing-library

I have come across the following issue in my project setup. Whenever I extend the httpService and use 'this.instance' in any service, an error occurs. On the other hand, if I use axios.get directly without any interceptors in my service files, i ...

In Rxjs, ConcatMap doesn't get executed

I am currently developing a file upload component that involves checking for the existence of a file before proceeding with the upload process. If the file already exists, the user is given the option to either save as a copy or overwrite the existing file ...

What steps can I take to ensure my classes are not tied to a specific platform?

Currently, I am facing an issue with my GPS module. The module can detect the type of message on the fly and configure them if necessary. I achieved this by composing several classes. To reduce dependency on the platform (stm32), I introduced an IStreamD ...

What is the best way to effectively nest components with the Nebular UI Kit?

I'm currently facing an issue with nesting Nebular UI Kit components within my Angular app. Specifically, I am trying to nest a header component inside the home page component. The problem arises when the attributes take up the entire page by default, ...

Update the class attributes to a JSON string encoding the new values

I have created a new class with the following properties: ''' import { Deserializable } from '../deserializable'; export class Outdoor implements Deserializable { ActualTemp: number; TargetTemp: number; Day: number; ...

Tips for building an interface in TypeScript with a restricted range of indices

I'm working on a function that accepts parameters of type Record<string, string>. How can I define a type with a limited set of indexes without triggering a TypeScript compiler error? Is there a way to create an interface with only specific ind ...

What is the process for enabling keyboard selections in a Material-UI Select component?

Is there a way to make the MUI Select component keyboard accessible by tabbing into it and using the first letter of the option to auto-select without opening the list? I am looking for a feature where pressing the initial letter selects the first item tha ...

The Jest test is experiencing a failure as a result of an imported service from a .ts file

In my Jest test file with a .spec.js extension, I am importing an index.js file that I need to test. Here is the code snippet from the .spec.js file: var HttpService = require('./index.js').HttpService; The problem arises because the index.js f ...

Unable to access the values of the object within the form

I am encountering an issue where I cannot retrieve object values in the form for editing/updating. The specific error message is as follows: ERROR TypeError: Cannot read properties of undefined (reading 'productName') at UpdateProductComponen ...

Anticipating the outcome of various observables within a loop

I'm facing a problem that I can't seem to solve because my knowledge of RxJs is limited. I've set up a file input for users to select an XLSX file (a spreadsheet) in order to import data into the database. Once the user confirms the file, v ...

Collaborate on sharing CSS and TypeScript code between multiple projects to

I am looking for a solution to efficiently share CSS and TS code across multiple Angular projects. Simply copy-pasting the code is not an ideal option. Is there a better way to achieve this? ...