Search code examples
javascripttypescriptdesign-patternscircular-dependencyrollup

Client-side typescript: how do I remove circular dependencies from the factory method pattern?


I am using the factory method pattern in some of my code. The problem is, some of those instances also use the same factory method pattern. This creates circular dependencies and I can't think of a way of removing them. Let me give an example:

// factoryMethod.ts
import Instance1 from './Instance1';
import Instance2 from './Instance2';
import Instance3 from './Instance3';
import { Instance, InstanceName } from './Instance';

export const getInstanceByName = (
  instanceName: InstanceName
): Instance => {
  switch (instanceName) {
    case 'Instance1':
      return Instance1;
    case 'Instance2':
      return Instance2;
    case 'Instance3':
      return Instance3;
    default:
      throw new Error();
  }
};

// extremelyHelpfulUtilityFunction.ts
import { getInstanceByName } from './factoryMethod';

export const extremelyHelpfulUtilityFunction = (instanceName: InstanceName): number => {
  // Imagine this was an extremely helpful utility function
  return getInstanceByName(instanceName).num
}

// Instance.ts
export interface Instance {
  doSomething: () => number;
  num: number;
}

export type InstanceName = 'Instance1' | 'Instance2' | 'Instance3';

// Instance1.ts
import { extremelyHelpfulUtilityFunction } from './extremelyHelpfulUtilityFunction';

const i: Instance = {
  doSomething: (): number => {
    return extremelyHelpfulUtilityFunction('Instance2') + extremelyHelpfulUtilityFunction('Instance3'); // circular dependency
  },
}
export default i;

// Other instances defined below, you get the idea.

I'm using rollup to turn this into a single JavaScript file, and when I do, it warns me that I have a circular dependency. I want to get rid of this warning. I realize the code will still function, but I don't want the warning there. How can I modify this code so that InstanceX can get InstanceY without it being a circular dependency?


Solution

  • IMO the problem is that extremelyHelpfulUtilityFunction has to know getInstanceByName, while the result of this factory could always be known in advance by the caller and the desired value passed as argument to the helper.

    I would propose

    // Instance1.ts
    const instance1: Instance = {
      doSomething: (): number => {
        return (new Instance2()).toNum() + (new Instance3()).toNum()
      },
    }
    

    with toNum defined in Instance.ts and overridden in its subclasses, using the helper but with proper parameters, for example

    // Instance2.ts
    const instance2: Instance = {
      doSomething: ...,
      toNum: (): number => {
        return extremelyHelpfulUtilityFunction(1234)
      }
    }
    

    where you would use this.num instead of 1234 if you declared a proper class for Instance2 instead of this object, like

    // Instance2.ts
    class Instance2 extends Instance {
      num = 1234;
      doSomething: ...
      toNum(): number {
        return extremelyHelpfulUtilityFunction(this.num)
      }
    }
    export default new Instance2();