Does the Factory Pattern in js violate the Open-Close principle?

Viewed 139

since JS does not support abstract classes or inheritance, every time we want to add a new type to be created when using factory pattern, we will have to modify the code which mean we violate the open-close principle. for example, in the snapshot bellow - if we want to add a new employee type like Marketing, we will have to update the switch statement which is violation of the open-close principle. Is there any workaround to use the factory pattern without violation open-close principle?

function Accountant(){
    console.log('I am accountant');
}

function Developer(){
    console.log('I am developer');
}

function Sales(){
    console.log('I am sales');
}

function CreateEmployee(employee){
    switch(employee){
        case('accountant'): return new Accountant();
        case('developer'): return new Developer()
        case('sales'): return new Sales();
    }
}
4 Answers

if we want to add a new employee type, we will have to update the switch statement which is violation of the open-close principle.

No, it doesn't. The OCP is not about forbidding to update code. If we want to implement a new feature, we of course need to touch the code of the program. The OCP is about designing your interfaces so that your program can be easily extended without changing code all over the place - ideally you only have to provide the new code, and change the configuration of the program to make use of it - or if this is not configurable, change only the high-level building blocks.

I would argue that the factory pattern even facilitates application of the OCP - don't think about changes to the factory function, think about the modules that use it. Instead of changing all the code in all the modules that instantiates employee objects, all you need to do is to supply a different factory to them.

A step in the right direction is to create an employeeType object that holds the constructors:

const employeeType = {
  Accountant,
  Developer,
  Salesperson
};

// console.log(new (employeeType["Accountant"])());
// Abstract away the hard-coded type above; the below randomization is strictly for demo purpose

const employeeTypeKeys = Object.keys(employeeType);
const employeeTypeIndex = Math.floor(employeeTypeKeys.length * Math.random());
console.log(new (employeeType[employeeTypeKeys[employeeTypeIndex]])());

function Accountant(){
    console.log('I am an accountant');
}

function Developer(){
    console.log('I am a developer');
}

function Salesperson(){
    console.log('I am a salesperson');
}

I would argue the pattern isn't exactly something I'd like to extend but it's possible.

Say you have access to the CreateEmployee function alone and you'd want to extend it so you can also add Engineers.

import CreateEmployee from "./employee.js";

function Engineer(){
   console.log("I'm an Engineer");
}

function CreateEmployeeAndEngineer(employeeType){
    if(employeeType === 'Engineer') return new Engineer();
    else {
        return CreateEmployee(employeeType);
    }
}

Simple (ehh... not really) function composition.

However, there's very little value for it in Javascript since it's untyped. Then of course functions, and therefore constructors, are first-class citizens and can be easily passed down to the new operator.

since JS does not support abstract classes or inheritance

Javascript does support inheritance through it's concept of prototype chain.

You could implement the Factory method pattern if you'd want.

If you want new instances to be created on the fly, you should use Object literals. Review the following design for an idea on how you may, or may not, want to go about this:

function ucFirst(string){
  const s = string.split('');
  return s.shift().toUpperCase()+s.join('');
}
function InstanceController(){
  this.instances = [];
  this.add = (name, obj)=>{
    this.instances.push({name:name, obj:obj});
    return this;
  }
  this.get = name=>{
    for(let o of this.instances){
      if(o.name === name){
        return o.obj;
      }
    }
    return false;
  }
  this.remove = name=>{
    for(let i=0,a=this.instances,l=a.length; i<l; i++){
      if(a[i].name === name){
         a.splice(i, 1);
        break;
      }
    }
    return this;
  }
}
const ic = new InstanceController;
const data1 = {
  data:'could be from database', 
  more:'sure there can be more data',
  numberTest: 2
}
const data2 = {test:'just a test'};
ic.add('developer', data1).add('accountant', {testing:'see'});
let dev = ic.get('developer'), aco = ic.get('accountant');
if(dev)console.log(dev);
if(aco)console.log(aco);
console.log(ic.get('nope'));
ic.remove('accountant'); aco = ic.get('accountant');
console.log(aco);

Related