2

I'm trying to figure out how to incorporate factory function closures and use Object.assign/Object.create inheritance at the same time. Probably simple, but I can't figure out how to do it. First I build my desired factory that utilizes closure. I have methods that act on my private variables.

const projectFactory = function() {
    let _title = "new project";

    const _tabs = [];

    const getTitle = function() {
        return _title;
    }

    const setTitle = function(input) {
        _title = input;
        return;
    }

    const createTab = function() {
        // some method that fills the tabs array
        return;
    }

    return {
        getTitle,
        setTitle,
        createTab
    }
};

const factory1 = projectFactory(); 
console.log(factory1); // methods set on object, _title and _tabs not visible

`

Well all my methods are going to be the same for all these objects I'm creating when calling this factory function. So I decided to pull out my methods and store them in an object to be referenced then rewrite my factory function using Object.assign / Object.create, this way the methods are inherited.

const projectMethods = {
    getTitle() {
        return this.title;
    },
    setTitle(input) {
        this.title = input;
        return;
    },
    createTab() {
        // some method that fills the tabs array
        return;
    }
};
const projectFactory = function() {
    let title = "new project";

    const tabs = [];

    return Object.assign(Object.create(projectMethods), {
        title, tabs
    });
};

const factory1 = projectFactory();
console.log(factory1); // methods available on prototype, title and tabs visible

But now since I'm returning a whole object in my factory I no longer have private variables. How can I achieve the result:

console.log(factory1); // methods available on prototype, title and tabs not visible
Peter Seliger
  • 11,747
  • 3
  • 28
  • 37
stuffz
  • 23
  • 4
  • You cannot have private variables **and** shared inherited methods at the same time. It's just impossible to have a shared closure. – Bergi Jul 17 '23 at 22:37
  • 1
    @Bergi Oops thanks, edited to what I had intended. I expected that that may have been the case. Thanks for your input. – stuffz Jul 17 '23 at 23:07
  • 1
    @stuffz ... there is actually a pattern which allows shared (locally enclosed / private) state amongst modularized mixin functionality. It, of cause, is (has to be) based exclusively on closures. Therefore the one thing which does not work well together is _closures **and** `Object.assign`_. But closures only already is (would be) sufficient enough. – Peter Seliger Jul 18 '23 at 09:01
  • @PeterSeliger Sharing state between multiple methods of an instance is the easy thing. What is not possible is sharing the methods (via prototype inheritance). – Bergi Jul 18 '23 at 12:32
  • @Bergi ... sure. Prototypal methods anyhow do not make that much sense for the code base shown by the OP. And because of that my example code did drop anything related to inheritance in favor of a function-based mixin-approach. The latter on the other hand allows both, **a)** methods which work within a `this` context and **b)** shared private state amongst such mixin-modules. (Yet, a `Map` or `WeakMap` based implementation with just a little glue-code should be able to process local/private state via prototypal methods.) – Peter Seliger Jul 18 '23 at 12:51
  • @stuffz ... Regarding any of the approaches/solutions within the so far provided answers, are there any questions left? – Peter Seliger Jul 21 '23 at 07:46
  • @PeterSeliger Thanks a ton for your great answer. I understand the first iteration well and need more time / knowledge to parse through the second iteration and the alternative WeakMap solution as a beginner. As I understand to achieve the goal using ES6 classes there is now a private class feature: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes/Private_class_fields Which seems way more ergonomic. Are there downsides to using this besides the usual discourse of ES6 classes versus factory functions? – stuffz Jul 21 '23 at 20:55
  • 1
    @stuffz ... Private class fields are only possible within a `class` construct where the fields and the prototypal accessor functions have to be provided at the same time within the `class` body. Therefore, shared private fields are not possible for mixed-in functionality at a base class which features such fields and also does apply any kind of mixin. – Peter Seliger Jul 21 '23 at 21:29

2 Answers2

1

There is actually a pattern which allows shared (locally enclosed / private) state amongst modularized mixin-functionality/behavior. It, of cause, has to be based exclusively on closures. Therefore the one thing which does not work well together is closures and Object.assign where the latter happens outside of such closures (like shown with the OP's example code). On the other hand a closures-only approach already is sufficient enough.

Applying such a pattern to a first iteration, the OP's presented code could be implemented like ...

// function based mixin which incorporates shared state.
function withStatfulProjectBehavior(sharedState) {
  const project = this;

  function getTitle() {
    return sharedState.title;
  }
  function setTitle(value) {
    return (sharedState.title = value);
  }

  function createTab() {
    // some method that fills the tabs array ... e.g. ...
    return sharedState.tabs.push({
      title: 'new tab',
    });
  }

  return Object.assign(project, {
    getTitle,
    setTitle,
    createTab,
  });
}

// `project` factory function.
function createProject() {
  const sharedState = {
    tabs: [],
    title: 'new project',
  }
  const project = {};

  return withStatfulProjectBehavior.call(project, sharedState);
};

const project = createProject();
console.log({ project });

console.log(
  'project.getTitle() ...',
  project.getTitle()
);
console.log(
  "project.setTitle('Foo Bar') ...",
  project.setTitle('Foo Bar')
);
console.log(
  'project.getTitle() ...',
  project.getTitle()
);

console.log(
  'project.createTab() ...',
  project.createTab()
);
.as-console-wrapper { min-height: 100%!important; top: 0; }

And a 2nd iteration's outcome then already could look like ...

// helper functionality
function validateTitle(value) {
  if (typeof value !== 'string') {
    // throw new TypeError('A title has to by a string value.');
    console.warn('A title has to by a string value.');
    return false;
  } else {
    return true;
  }
}

// - function based, context aware mixin which applies generically
//   implemented protected property access at its context and over
//   shared state.
function withSharedProtectedProperty(
  state, { key, validation, enumerable = false }
) {
  const type = this;

  Reflect.defineProperty(type, key, {
    get: () => state[key],
    set: value => {
      if (validation(value)) {
        return state[key] = value;
      }
    },
    enumerable,
  });
}

// - function based, context aware mixin which applies tab
//   specific behavior at its context and over shared state.
function withSharedTabManagement(state) {
  this.addTab = function addTab(title = 'unnamed tab') {
    // some method that fills the tabs array ... e.g. ...
    return state.tabs.push({ title });
  };
}

// `project` factory function.
function createProject() {
  const state = {
    tabs: [],
    title: 'new project',
  }
  const project = {};

  withSharedProtectedProperty.call(project, state, {
    enumerable: true,
    key: 'title',
    validation: validateTitle,
  });
  withSharedTabManagement.call(project, state);

  return project;
};

const project = createProject();
console.log({ project });

console.log(
  'project.title ...', project.title
);
console.log(
  "project.title = 1234 ...", (project.title = 1234)
);
console.log(
  'project.title ...', project.title
);
console.log(
  "project.title = 'Foo Bar' ...", (project.title = 'Foo Bar')
);
console.log(
  'project.title ...', project.title
);

console.log(
  'project.addTab() ...', project.addTab()
);
.as-console-wrapper { min-height: 100%!important; top: 0; }

One also might have a look at ...

Edit ... in order to take into account some of Bergi's comments ...

"[@stuffz] ... You cannot have private variables and shared inherited methods at the same time. It's just impossible to have a shared closure." – Bergi

"@PeterSeliger Sharing state between multiple methods of an instance is the easy thing. What is not possible is sharing the methods (via prototype inheritance)." – Bergi

... where one of my replied sentences stated ...

"... (Yet, a Map or WeakMap based implementation with just a little glue-code should be able to process local/private state via prototypal methods.)" – Peter Seliger

... any direct implementation of the OP's original example code, based on the above mentioned glue-code, then should be very close to ...

// project specific module scope.

// state management via `WeakMap` instance.
function getState(reference) {
  return projectStates.get(reference);
}
const projectStates = new WeakMap;

// object based mixin of stateful (later prototypal) project behavior.
const statefulProjectMethods = {
  getTitle() {
    return getState(this).title;
  },
  setTitle(input) {
    return getState(this).title = input;
  },
  createTab(title = 'unnamed tab') {
    // some method that fills the tabs array ... e.g. ...
    return getState(this).tabs.push({ title });
  },
};

// `project` factory function.
/*export */function createProject(protoMethods) {
  const project = Object.create(protoMethods);

  projectStates.set(project, {
    tabs: [],
    title: 'new project',
  });
  return project;
};
// end of project specific module scope.


// another module's scope
// import createProject from '/project.js';
const project = createProject(statefulProjectMethods);

console.log(
  "project.hasOwnProperty('getTitle') ...", project.hasOwnProperty('getTitle')
);
console.log(
  "project.hasOwnProperty('setTitle') ...", project.hasOwnProperty('setTitle')
);
console.log(
  "project.hasOwnProperty('createTab') ...", project.hasOwnProperty('createTab')
);
console.log(
  "Object.getPrototypeOf(project) ...", Object.getPrototypeOf(project)
);

console.log(
  'project.getTitle() ...', project.getTitle()
);
console.log(
  "project.setTitle('Foo Bar') ...", project.setTitle('Foo Bar')
);
console.log(
  'project.getTitle() ...', project.getTitle()
);

console.log(
  'project.createTab() ...', project.createTab()
);
.as-console-wrapper { min-height: 100%!important; top: 0; }
Peter Seliger
  • 11,747
  • 3
  • 28
  • 37
-1

state monad

Maybe you're looking for the state monad. The first example in Peter's post is similar to how the state monad works. The primary difference is state changes do not mutate previous state.

Let's first design the initial state of our project -

const initProject = {
  title: "<insert title>",
  tabs: [{ title: "Untitled.txt" }]
}

Now we can write a function that updates the title -

const setTitle = title =>
  State.get().bind(project => // get the state, bind to "project"
    State.put({...project, title}) // set new state
  )

Here's one to create a new tab -

const createTab = title =>
  State.get().bind(project => // get the state, bind to "project"
    State.put({...project, tabs: [...project.tabs, {title}]}) // set new state
  )

The bind function of any monad allows us to read the contained value, perform some computation with it, and return a new monad encapsulating the result of that computation -

console.log(
  setTitle("hello world")
    .bind(() => createTab("2"))
    .bind(() => createTab("3"))
    .execState(initState)
)
{
  "title": "hello world",
  "tabs": [
    {
      "title": "Untitled.txt"
    },
    {
      "title": "2"
    },
    {
      "title": "3"
    }
  ]
}

Let's run a code example to check our progress. Don't worry about understanding State for now -

// state monad
const State = Object.assign(
  runState => ({
    runState,
    bind: f => State(s => {
      let {value, state} = runState(s)
      return f(value).runState(state)
    }),
    evalState: s => runState(s).value,
    execState: s => runState(s).state,
  }),
  {
    return: y => State(x => ({value: y, state: x})),
    get: () => State(x => ({value: x, state: x})),
    put: x => State(_ => ({value: null, state: x})),
  },
)

// your methods
const setTitle = title =>
  State.get().bind(project =>
    State.put({...project, title})
  )

const createTab = title =>
  State.get().bind(project =>
    State.put({...project, tabs: [...project.tabs, {title}]})
  )

// your program
const initState = {
  title: "<insert title>",
  tabs: [{ title: "Untitled.txt" }],
}

console.log(
  setTitle("hello world")
    .bind(() => createTab("2"))
    .bind(() => createTab("3"))
    .execState(initState)
)

console.log(
  createTab("⚠️")
    .bind(() => createTab("⚠️"))
    .execState(initState)
)
.as-console-wrapper { min-height: 100%; top: 0 }
{
  "title": "hello world",
  "tabs": [
    {
      "title": "Untitled.txt"
    },
    {
      "title": "2"
    },
    {
      "title": "3"
    }
  ]
}
{
  "title": "<insert title>",
  "tabs": [
    {
      "title": "Untitled.txt"
    },
    {
      "title": "⚠️"
    },
    {
      "title": "⚠️"
    }
  ]
}

too much .bind!

The .bind(project => ...) allows you to read the state, similar to how Promise .then(value => ...) allows you to read the value of a promise, but these closures can be a burden to work with. Much like Promise has async..await, we can implement State.run to eliminate need for .bind closures -

const setTitle = title => State.run(function *() {
  const project = yield State.get() // State.get().bind(project => ...
  return State.put({...project, title})
})

const createTab = title => State.run(function *() {
  const project = yield State.get() // State.get().bind(project => ...
  return State.put({...project, tabs: [...project.tabs, {title}]})
})

The benefit is observed when more .bind calls are saved. If the result of the bind is not needed, you can leave the LHS of yield empty -

State.run(function *() {
  yield setTitle("hello world") // setTitle("hello world").bind(() => ...
  yield createTab("2")          // createTab("2").bind(() => ...
  return createTab("3")
})

This updated demo using State.run produces the same result without the need for .bind closures -

// state monad
const State = Object.assign(
  runState => ({
    runState,
    bind: f => State(s => {
      let {value, state} = runState(s)
      return f(value).runState(state)
    }),
    evalState: s => runState(s).value,
    execState: s => runState(s).state,
  }),
  {
    return: y => State(x => ({value: y, state: x})),
    get: () => State(x => ({value: x, state: x})),
    put: x => State(_ => ({value: null, state: x})),
    run: e => {
      const g = e()
      const next = x => {
        let {done, value} = g.next(x)
        return done ? value : value.bind(next)
      }
      return next()
    },
  },
)

// your methods
const setTitle = title => State.run(function *() {
  const project = yield State.get()
  return State.put({...project, title})
})

const createTab = title => State.run(function *() {
  const project = yield State.get()
  return State.put({...project, tabs: [...project.tabs, {title}]})
})

// your program
const initProject = {
  title: "<insert title>",
  tabs: [{ title: "Untitled.txt" }],
}

console.log(State.run(function *() {
  yield setTitle("hello world")
  yield createTab("2")
  return createTab("3")
}).execState(initProject))

console.log(State.run(function *() {
  yield createTab("⚠️")
  return createTab("⚠️")
}).execState(initProject))
.as-console-wrapper { min-height: 100%; top: 0 }
{
  "title": "hello world",
  "tabs": [
    {
      "title": "Untitled.txt"
    },
    {
      "title": "2"
    },
    {
      "title": "3"
    }
  ]
}
{
  "title": "<insert title>",
  "tabs": [
    {
      "title": "Untitled.txt"
    },
    {
      "title": "⚠️"
    },
    {
      "title": "⚠️"
    }
  ]
}

related

You may find other useful details about the state monad in an older post.

Mulan
  • 129,518
  • 31
  • 228
  • 259
  • "*Maybe you're looking for the state monad*" - I don't think so – Bergi Jul 19 '23 at 22:23
  • ouf.. the functions are shared between instances, share access to the same data, the program produces a valid output, by use of closures. maybe you could be a little more precise with your critique? – Mulan Jul 20 '23 at 02:17
  • Nothing specific, it's just that I believe *this answer is not useful* for the OP who wanted to learn about factory functions and prototypical inheritance. I didn't want to downvote anonymously because of the effort that went into the answer, but I'm not inclined to discuss or change my opinion. – Bergi Jul 20 '23 at 03:13