2

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();
    }
}
Dexygen
  • 12,287
  • 13
  • 80
  • 147
Alex
  • 53
  • 4
  • An Object Literal is a new separate instance. – StackSlave Jun 07 '21 at 21:24
  • 1
    [Abstract Classes in Javascript](https://www.educba.com/abstract-classes-in-javascript/) – charlietfl Jun 07 '21 at 22:02
  • 2
    "*JS does not support abstract classes or inheritance*" - [wrong](https://stackoverflow.com/q/29480569/1048572) and [wrong](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Inheritance_and_the_prototype_chain). But neither of these have anything to do with the OCP. "Open for extension" does not refer to subclassing. – Bergi Jun 07 '21 at 22:32

4 Answers4

4

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.

Bergi
  • 630,263
  • 148
  • 957
  • 1,375
  • I find this answer somewhat conflicted. In particular, "_...to implement a new feature, we of course need to touch the code..._" is followed by describing solutions that do not touch the code. – jaco0646 Jun 08 '21 at 00:20
  • @jaco0646 I was referring to the *whole* program (including configuration), something always needs to be touched to change something. Also, if I write a *new program* repurposing many parts of the old one ("not changing their file contents"), is it [still the same (but altered) program](https://en.wikipedia.org/wiki/Ship_of_Theseus)? What does it even mean to "update code"? :-) – Bergi Jun 08 '21 at 00:27
  • Philosophy aside, in the context of the OCP there is a clear delineation between creating new code and editing old code. The principle specifically advocates the former in preference to the latter. While I think this answer is mainly correct, I think it may leave the OP with more confusion around the OCP. The OP appears to be aware that, "_...all you need to do is to supply a different factory_". The question is how to do that in JavaScript. – jaco0646 Jun 08 '21 at 00:40
  • @jaco0646 I'm not so sure they were aware of that. And they haven't shown the code that uses (used) the factory. – Bergi Jun 08 '21 at 00:43
2

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');
}
Dexygen
  • 12,287
  • 13
  • 80
  • 147
1

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.

MinusFour
  • 13,913
  • 3
  • 30
  • 39
-3

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);
StackSlave
  • 10,613
  • 2
  • 18
  • 35