Skip to content

Instantly share code, notes, and snippets.

@cristianp6
Last active September 17, 2024 08:31
Show Gist options
  • Save cristianp6/e11208c954d90d945fe98ef2e61f8926 to your computer and use it in GitHub Desktop.
Save cristianp6/e11208c954d90d945fe98ef2e61f8926 to your computer and use it in GitHub Desktop.
Make Factories not Classes

For the KISS principle, you can opt for the factory function pattern (aka closures) instead of use classes.

// Typings definition (stripped out at complie time):
type MyObject = {
  readonly aPublicMethod: () => void;
  readonly anotherPublicMethod: (anArgument: any) => any;
};

type MyObjectFactory = (aDependencyInstance: any, anotherDependencyInstance: any) => MyObject;


// Actual implementation:
const createMyObject: MyObjectFactory = (aDependencyInstance, anotherDependencyInstance) => {
  // All "private" logic here, not visible outside
  // Think it as the constructor or the class context
  
  const aPrivateMethod = () => {
    return aDependencyInstance; // Do stuff here
  };

  // Public methods are those returned
  return {
    aPublicMethod: () => {
      return aPrivateMethod();
    },
    anotherPublicMethod: (anArgument) => {
      return anotherDependencyInstance[anArgument]; // Do stuff here
    }
  }
}


// Usage:
// Dependencies instances
const firstDependencyInstance = {};
const secondDependencyInstance = [];
// Instantiate the object, think it like "new MyObject(...)"
const myObj = createMyObject(firstDependencyInstance, secondDependencyInstance);
myObj.aPublicMethod();
myObj.anotherPublicMethod('myArgument');

Conventions:

  • define one factory per file with typings definition at the beginning of it to know right away what's inside;
  • postpone Factory to factory's typing declaration name and prepend create to method's implementation name;
  • inject all dependencies needed to have more "portable" code with arguably no side effects;
  • return only methods you want to be public: declare them as readonly to prevent "change of implementation" errors at compile-time and, if it's required to grant object immutability at runtime, wrap return statement with Object.freeze({ ... }) (beware is not a deep freeze but just at "root level").

Benefits:

  • less code is produced by transpiling to ES5 (functions are "native", classes require polyfills ...to functions);
  • make internal things really private: classes private fields still require polyfills and will be public once the code is transpiled;
  • easier to define typings and group them for better readability;
  • avoid this context workarounds (eg. bind, that = this);
  • may be easier to test;
  • promotes composition over inheritance: extends means coupling things up, something you should do consciously because can bring to mutation, side effects, responsibility overload;
  • memory wise, functions are added in the prototype each time are created, so may be better to use Class when creating thousands of the same object because just one instance will be in the prototype (if the target is ES6+);
  • not convinced yet? 1, 2, 3, 4
type Robot = {
readonly walk: () => void;
readonly speak: (words: string[]) => string;
};
type RobotFactory = (types: RobotTypes, type: string) => Robot;
const createRobot: RobotFactory = (types, type) => {
// All "private" logic here
const name = (type: string) => {
return types[type].name;
};
// Only what is returned is public
return Object.freeze({
walk: () => {
console.log(`${name(type)} is walking`);
},
speak: (words) => {
return `${name(type)} says: "${words}"`;
}
});
};
interface RobotType { name: string };
interface RobotTypes { [key: string]: RobotType };
const robotTypes: RobotTypes = {
mazinger: { name: 'Mazinger Z' },
transformer: { name: 'Optimus Prime' },
voltron: { name: 'Voltron' }
};
const transformer = createRobot(robotTypes, 'transformer');
transformer.walk();
const transformerSays = transformer.speak(['hi']);
console.log(transformerSays);
const voltron = createRobot(robotTypes, 'voltron');
voltron.walk();
const voltronSays = voltron.speak(['bye', 'bye']);
console.log(voltronSays);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment