5

I would like to implement Strategy pattern in Angular/Typescript and use a service in component; service takes a strategy interface as constructor parameter. Also, constraint is that service is dependent on some other injected service at angular level.

I'm digging through the docs but unable to find a way to do it. I would like not to end up in over-engineered code, searching for simple solution to have a Strategy pattern implemented.

Please see below in mocked-up code:

export interface IStrategy {
    calculate(a,b): number;
}

export class MinusStrategy implements IStrategy {
    calculate(a,b): number {
        return a - b;
    }
}

export class PlusStrategy implements IStrategy {
    calculate(a,b): number {
        return a + b;
    }
}

@Injectable() 
export class CalculatorService {
    // I understand it is not possible to have DI of the Interface here for calcStrategy 
    constructor(private http: HttpClient; private calcStrategy: IStrategy);
    
    getByCalc() {
        this.http.get('someurl?calc=' + calcStrategy.calculate(1,1));
    }
}

@Component(// skipped config here...)
export class MyComponent implements OnInit {
    // How to inject appropriate concrete strategy (and either autowire or provide httpClient?)
    constructor(private service: new CalculatorService(httpClient, new MinusStrategy()));
    
    useInComponent() {
        this.service.getByCalc();
    }
}

NenadP
  • 585
  • 7
  • 24

4 Answers4

4

My two cents - You cannot rely on DI to provide concrete instance in such case. DI has no way to know which type of instance is needed in each context.

I'd suggest using factory pattern here. For example -

@Injectable()
export class StrategyFactory {
  createStrategy<T extends IStrategy>(c: new () => T): T {
    return new c();
  }
} 

//then you can inject StrategyFactory in your service class, use it like -
factory.createStrategy(MinusStrategy).calculate(2, 1); 
ch_g
  • 1,394
  • 8
  • 12
  • 1
    Interesting approach. My intention is to provide `CalculatorService` to `MyComponent` though. But your approach gives me idea, like this: Provide a Factory managed by Angular, but not others. `@Injectable() export class CalculatorFactory { constructor(private http: HttpClient) {} createCalculator(strategy: IStrategy): Calculator { return new Calculator(this.http, strategy);}}` Calculator (now CalculatorService) is then used in component: `const calculator = this.calculatorFactory.createFactory(new MinusStrategy()); calculator.getByCalc();` – NenadP Jul 11 '20 at 23:06
  • The drawback of this approach is that you need to specify the type of concrete object in the components / services. If you need a global change than you have to go through all the sevices and components and change the strategy. For example today you are using azure authentication strategy and next week you need to use auth0 strategy. – Benjamin Martin Jun 14 '23 at 13:54
2

You need to use abstract class instead of interface for "IStrategy". Because Angular don't support an interface as token for injection. ( https://angular.io/guide/dependency-injection-providers#non-class-dependencies ) . After that, You can define in providers of module as below

{ provide: IStrategy, useClass: MinusStrategy }

After that, The CalculatorService will use MinusStrategy to inject to any component in that module which injected the service.

export abstract class IStrategy {
    abstract calculate(a,b): number;
}

export class PlusStrategy extends IStrategy {
    calculate(a,b): number {
        return a + b;
    }
}

export class MinusStrategy extends IStrategy {

    calculate(a,b): number {
        return a - b;
    }
}

@Injectable({
  providedIn: 'root',
})
export class CalculatorService {

  constructor(
    private http: HttpClient, 
    private calcStrategy: IStrategy) {};
    
    getByCalc() {
        console.log(`Result is: ${this.calcStrategy.calculate(1,1)}`);
    }
}

//The module need to add token to providers for Strategy classes.
@NgModule({
  declarations: [
    ...
  ],
  imports: [
    ...
  ],
  providers: [
    { provide: IStrategy, useClass: MinusStrategy }
  ],
  bootstrap: [...]
})
export class AppModule { }

Note that I keep "IStrategy" name for abstract class for example. It should be "BaseStrategy" or something else.

============================================================

[ 07 / 12 / 2020 17:00:00 GMT+7 ]

I created a demo project and update my approach follow this issue. Could you open it and see again for my approach.

Stackblitz Link: https://stackblitz.com/github/sangnt-developer/demo-injection-in-component-level

Github link: https://github.com/sangnt-developer/demo-injection-in-component-level

  • 1
    Hey Sang, can you provide how that would look like at `@Component`? I don't see it in your example, please see my code above. `Component` will need to decide which Strategy to use, in your example you hard-wire it on module level always to use MinusStrategy? (One component might use `MinusStrategy`, but other, in the same module might use `PlusStrategy`. Thanks! – NenadP Jul 11 '20 at 20:02
  • I'm here @NenadP . I updated my answer with code example, open and see it again. Let me know if have any questions :D – Sang Nguyen Jul 12 '20 at 10:05
  • If you want strategy A in a component and in a service strategy B and in another service strategy C, you cannot. Since in all services you have to use the one declared at module level. – Mihai Socaciu Oct 17 '22 at 15:57
1

One way to do this is to define a custom injection token and use this token in your components provider declaration, (see https://angular.io/guide/dependency-injection-in-action#supply-a-custom-provider-with-inject for more information):

export const CalculatorStrategy = new InjectionToken<string>('CalculatorStrategy');

@Component({
    providers: [
        // define the actual strategy-implementation here, setting it as `useClass`-provider
        {provide: CalculatorStrategy, useClass: PlusStrategy}
    ]
})
export class MyComponent implements OnInit {
    
    constructor(private service: CalculatorService) {
    }

    useInComponent() {
        this.service.getByCalc();
    }
}

@Injectable()
export class CalculatorService {

    constructor(private http: HttpClient, @Inject(CalculatorStrategy) private calcStrategy: IStrategy);

    getByCalc() {
        this.http.get('someurl?calc=' + this.calcStrategy.calculate(1, 1));
    }
}
eol
  • 23,236
  • 5
  • 46
  • 64
  • Hey thanks, I am nearly there with your help! I am getting: ```InjectionToken MinusStrategy]: NullInjectorError: No provider for InjectionToken MinusStrategy!``` If I provide it on module level like: ```{ provide: CalculatorStrategy, useClass: MinusStrategy }``` But then I need to provide a token for each Concrete implementation there? I hoped I could swap it dinamicaly as you showed? – NenadP Jul 11 '20 at 18:41
  • Hmm actually I'm pretty sure this should work, can't test it right now unfortunately. In the meantime maybe check this: https://stackoverflow.com/a/37002587/3761628 -> he's doing something quite similar. – eol Jul 11 '20 at 18:51
  • It seems I cannot get it to work on Service level. The NullInjector error is thrown. I can provide injection token with concrete implementation there, but then it defeats is purpose, as I want client code (component) to decide which strategy to use. If you have time, you could maybe provide Fiddle / Plunker? Thanks – NenadP Jul 11 '20 at 19:57
  • I see - I'll have to check once I'm no longer on mobile :) Maybe check Sang's solution as well though. – eol Jul 11 '20 at 20:02
  • Sure, I am checking his approach as well. I would prefer to use interfaces if possible though. – NenadP Jul 11 '20 at 20:03
1

I think it can achived on component level by adding the service and the token on the providers section.

 @Component({
    ...
    ...
    proverders: [
    CalculatorService,
    {provide: IStrategy, useClass: 
    PlusStrategy
    ]
    })

Then inject the service on the constructor

Arghya Sadhu
  • 41,002
  • 9
  • 78
  • 107
Moli
  • 11
  • 3