Search code examples
nestjsfactory-patternsolid-principles

How to use factory pattern correctly in nestjs


In Nestjs, I have a Module, which using useFactory to dynamically create class base on configValue

There is no dedicated service in that module, instead, it return a service that depends on config, therefore the DriverService1 and DriverService2 will construct together

export const createFactory = (config: IConfig):Provider<IService> => {
    return {
        provide: 'FACTORY',
        useFactory: (service1: DriverService1, service2: DriverService2): IService => {
            if (config.driver == 'DriverService1')
            {
                return service1;
            }
            else if (config.driver == 'DriverService2')
            {
                return service2;
            }
            throw new Error('not implemented')
        },
        inject: [ DriverService1, DriverService2 ],
    }
};

@Module({})
export class MyModule {
    static register(config?: IConfig): DynamicModule {
        const factory = createFactory(config)
        return {
            module: MyModule,
            providers: [
                {
                   provide: 'CONFIG',
                   useValue: config,
                },
                DriverService1,
                DriverService2,
                factory
            ],
            exports: [factory],
        };
    }
}

but im not sure is it a correct way to do that

or i should create a dedicated service in this module , e.g "MyModuleService", and then do the factory pattern inside the service? which the driver will only construct when it use



interface IDriver {
    action1():void
    action2():void
    action3():void
}

class Driver1 implements IDriver{
    public action1():void {
        console.log("DriverService1 action1")
    }
    public action2():void {
        console.log("DriverService1 action2")
    }
    public action3():void {
        console.log("DriverService1 action3")
    }
}

class Driver2 implements IDriver{
    public action1():void {
        console.log("DriverService2 action1")
    }
    public action2():void {
        console.log("DriverService2 action2")
    }
    public action3():void {
        console.log("DriverService2 action3")
    }
}


export const createFactory = (config: IConfig):Provider<MyModuleSerice> => {
    return {
        provide: 'BROKER_FACTORY',
        useFactory: (service:MyModuleSerice): MyModuleSerice => {

            if (config.driver == 'Driver1')
            {
                service.setDriver(new Driver1());
            }
            else if (config.driver == 'Driver2')
            {
                service.setDriver(new Driver2());
            }
            else{
                throw new Error('not implemented')
            }
            return service
        },
        inject: [ MyModuleSerice ],
    }
};


@Module({})
export class MyModule {
    static register(config?: IConfig): DynamicModule {
        const facotry = createFactory(config)
        return {
            module: MyModule,
            providers: [
                {
                    provide: 'CONFIG',
                    useValue: config,
                },
                facotry
            ],
            exports: [facotry],
        };
    }
}

@Injectable()
class MyModuleSerice {
    protected driver:IDriver
    constructor() {
    }

    public setDriver(driver:IDriver) {
        this.driver = driver
    }
    
    public doSomething():void {
        this.driver.action1()
        this.driver.action2()
    }

    public doSomething2():void {
        this.driver.action1()
        this.driver.action3()
    }
}


Solution

  • Let's first understand the difference between two approaches before finalizing which one is better.

    1. Approach 1: You have created a module which will provide an instance of driver based on the config provided. This could be thought of as a DriverFactoryModule.

    2. Approach 2: You have created a module which returns a service that encapsulates an instance of driver (based on the config provided) and the main purpose is to provide functionality to perform action that could be performed using the driver instance. This approach does not expose the driver instance itself.

    In both the approaches, you intend to use factory pattern to create an instance of driver based on the user's need. However, the implementation of factory pattern relies on abstraction, that is the Factory class relies on the Interface or abstraction and creates the concrete instances based on user's request. Implementation wise, the factory pattern implementation follows this principle in the second approach. You can make another improvement by making changes to the implementation (e.g., DriverFactory service (internal to the module) with a method "getDriverInstance" which plays the role of a factory and use it inside the provider: (userFactory)). Additionally, the naming convention such as createFactory and provider token such as "BROKER_FACTORY" is incorrect as this method returns a service not a factory class instance.

    So, Now let's come to the main question - which approach is better? It totally depends on what is expected out of the module. Should the module just return the driver instances vs should it return a DriverService which encapsulate the driver instance