Reputation: 647
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();
}
}
Upvotes: 5
Views: 3723
Reputation: 11
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
Upvotes: 1
Reputation: 21
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
Upvotes: 2
Reputation: 1440
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);
Upvotes: 4
Reputation: 24565
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));
}
}
Upvotes: 1