Implementing strategy in nest.js
Asked Answered
I

1

5

I am trying to use the strategy pattern for the service, however the Module I try to use as context for strategy seems to only stick to one of the two. Here is the example code:

animal.module.ts

@Module({})
export class AnimalModule {
    static register(strategy): DynamicModule {
        return {
            module: AnimalModule,
            providers: [{ provide: 'STRATEGY', useValue: strategy }, AnimalService],
            imports: [],
            exports: [AnimalService]
        };
    }
}

animal.service.ts

@Injectable()
export class AnimalService {
    constructor (@Inject('STRATEGY') private strategy) {
        this.strategy = strategy
    }

    public makeSound() {
        return this.strategy.makeSound()
    }
}

cat.module.ts

@Module({
    imports: [
        AnimalModule.register(catStrategy),
    ],
    controllers: [CatController],
    providers: [CatService],
})
export class CatModule {}

cat.service.ts

@Injectable()
export class CatService {
    constructor(
        private readonly animalService: AnimalService,
    ) {}

    public makeSound() {
        return this.animalService.makeSound()
    }
}

dog.module.ts

@Module({
    imports: [
        AnimalModule.register(dogStrategy),
    ],
    controllers: [DogController],
    providers: [DogService],
})
export class DogModule {}

dog.service.ts

@Injectable()
export class DogService {
    constructor(
        private readonly animalService: AnimalService,
    ) {}

    public makeSound() {
        return this.animalService.makeSound()
    }
}

cat.strategy.ts

class CatStrategy {
    public makeSound() {
        return 'meow';
    }
}

export const catStrategy = new CatStrategy();

Repo that replicates the issue: https://github.com/kunukmak/nestjs-strategy-problem-example

To clarify, both catService.makeSound and dogService.makeSound return "meow" in this case. Is it possible to make the dog bark?

Indigo answered 22/4, 2021 at 17:5 Comment(0)
Z
3

I think you are looking for something like the following. Check the repo for the full example here. You can see below, we are registering a DynamicModule from the AnimalModule class:

@Module({
    imports: [AnimalModule.register()],
    controllers: [AppController],
    providers: [AppService],
})
export class AppModule {}

The DynamicModule returned from the register() call is responsible for determining what implementation of the AnimalModule to provide. This means we can customize the AnimalModule based on configuration in the environment.

@Module({})
export class AnimalModule {
    public static register(): DynamicModule {
        const AnimalClassProvider = AnimalModule.getClassProvider();
        return {
            module: AnimalModule,
            controllers: [AnimalController],
            providers: [AnimalClassProvider],
            exports: [AnimalClassProvider],
        };
    }

    private static getClassProvider(): ClassProvider<AnimalService> {
        const animalStrategy = process.env.ANIMAL_STRATEGY as AnimalStrategy;
        const AnimalServiceClass = AnimalModule.getClassFromStrategy(animalStrategy);
        return {
            provide: AnimalService,
            useClass: AnimalServiceClass,
        };
    }

    private static getClassFromStrategy(strategy: AnimalStrategy): Type<AnimalService> {
        switch (strategy) {
            case AnimalStrategy.CAT: return CatService;
            case AnimalStrategy.DOG: return DogService;
            default: return AnimalService;
        }
    }
}

AnimalStrategy in this case is just an enum used to determine which implementation of the service we should provide.

With this approach, we allow Nest to construct the Provider along with all its dependencies properly. We are only responsible for telling Nest which implementation it will construct when it encounters an AnimalService dependency. This allows the rest of our application to be unaware of the implementation and only use our AnimalService abstraction.

From our AnimalController:

@Controller('animal')
export class AnimalController {
    constructor(private readonly animalService: AnimalService) {}

    @Post()
    create(@Body() createAnimalDto: CreateAnimalDto) {
        return this.animalService.create(createAnimalDto);
    }

    // ...
}

to another service in our application:

@Injectable()
export class PetOwnerService {
    constructor(
        private readonly animalService: AnimalService,
        private readonly petOwnerService: PetOwnerService,
    ) {}

    feedPet(petName: string) {
        const petIsHungry = this.petOwnerService.isPetHungry(petName);
        if (petIsHungry) this.animalService.feed(petName);
        // ... 
    }
}
Zeidman answered 8/10, 2021 at 22:58 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.