1

This is the form of constructor which Douglas Crockford suggests in his book "How Javascript works" and in his lectures.

const constructor_x = function (spec) {
  let { a } = spec // private state
  
  // methods can modify private state
  const method_x = function () { a = '...' }
  
  // methods are exposed as public interface
  return Object.freeze({ method_x })
}

He suggests the following pattern for composition:

const constructor_y = function (spec) {
  let { b } = spec // private state

  // we can call other constructor and borrow functionality
  const { method_x } = constructor_x(spec)

  // we define new methods
  const method_y = function () { b = '...' }

  // we can merge borrowed and new functionality
  // and expose everything as public interface
  return Object.freeze({ method_x, method_y })
}

So here we see how to compose constructor_x and constructor_y. But my problem with this example (and all examples used when this pattern is presented) is that constructor_x and constructor_y make separate private states. constructor_x works on variable a, while constructor_y works on variable b. What if we want our constructors to share state? What if constructor_y also wants to work with variable a?

const constructor_y = function (spec) {
  let { a, b } = spec

  const { method_x } = constructor_x(spec)

  const method_y = function () { b = '...' }
  const method_z = function () { 
    // we may want to read `a` and maybe write to it
    a = '...' 
  }

  return Object.freeze({ method_x, method_y, method_z })
}

Of course this doesn't achieve what I want because a which constructor_y sees is not the same a constructor_x sees. If I used this, I could have achieved that maybe like so:

const constructor_x = function (spec) {
  return {
    _a: spec.a,
    method_x () { this._a = '...' }
  }
}

const constructor_y = function (spec) {
  return {
    ...constructor_x(spec),
    _b: spec.b
    method_y () { this._b = '...' },
    method_z () { this._a = '...' }
  }
}

But here I have lost privacy of variables _a and _b since they are attached to instance and are accessible just like methods. The best I can do is add underscore prefix which Douglas Crockford calls a sign of incompetence. I also lost instance's rigidness because it can no longer be frozen.

I could have exposed accessors for variable a in constructor_x like so:

const constructor_x = function (spec) {
  let { a } = spec // private state
  
  // methods can modify private state
  const method_x = function () { a = '...' }
  
  // methods are exposed as public interface
  return Object.freeze({ 
    method_x,
    get_a () { return a },
    set_a (val) { a = val }
  })
}

const constructor_y = function (spec) {
  let { a, b } = spec

  const { method_x, get_a, set_a } = constructor_x(spec)

  const method_y = function () { b = '...' }
  const method_z = function () { set_a('...') }

  return Object.freeze({ method_x, method_y, method_z })
}

These accessors can now be used by constructor_y to access private state of constructor_x. They are something like protected members in classical inheritance model. This makes constructor_x in some way special: It is not to be used as normal constructor, but only for composition inside other constructors. Another problem is that if we had another constructor like constructor_x which works on private variable a, we couldn't use them together in composition:

// another constructors which wants to work on `a`
const constructor_x2 = function (spec) => {
  let { a } = spec 

  const method_z = function () { a = '...' }

  return Object.freeze({ 
    method_z,
    get_a () { return a },
    set_a (val) { a = val }
  })
}

const constructor_y = function (spec) {
  let { a, b } = spec

  const { method_x, get_a, set_a } = constructor_x(spec)
  const { method_x2, get_a: get_a2, set_a: set_a2 } = constructor_x2(spec)

  // How do I use variable a now? There are two of them
  // and constructors x and x2 don't share them.
}

All of this would not be a problem if I used this and modified state on the instance.

  • 1/2 ... First, all this creator functions should be referred to as factories or factory functions. They are not constructors. ... *"What if we want our constructors to share state? "* ... then just implement the factories in a way that they can share each their entire inner/encapsulated state object and/or that they aggregate a shared state object while running the object creation process (the chained invocation of related functions during the composition process). – Peter Seliger Nov 19 '21 at 14:46
  • 2/2 ... Just take advantage of the language's flexibility and expressiveness. Just be aware of the advantages, pitfalls and comprehensibility (to others) of your modeling approach(es). And once this is checked don't worry about narrow minded Crockford disciples (or any other school/religion/cult). A good teacher shows you a pass and allows/encourages you to discover or follow your own, once you understood what the base/basics are good for. – Peter Seliger Nov 19 '21 at 14:56

1 Answers1

0

From my comments above ...

"1/2 ... First, all this creator functions should be referred to as factories or factory functions. They are not constructors. ... What if we want our constructors to share state? " ... then just implement the factories in a way that they can share each their entire inner/encapsulated state object and/or that they aggregate a shared state object while running the object creation process (the chained invocation of related functions during the composition process)."

What the OP wants to achieve can not be entirely covered by closure creating factory functionality according to Crockford / provided by the OP.

Encapsulated but shared (thus also mutable) state amongst "Function based Composable Units of Reuse" gets achieved best by a single factory which takes care of the composition process by invoking one or more mixin like functions which in addition to the to be shaped / aggregated type (the latter should carry public methods only) also need to get passed the type's local state(which will be accessed by the types's public methods).

function withActionControl(type, actionState) {
  actionState.isInAction = false;

  return Object.assign(type, {
    monitorActions() {
      const {
        isInAction,
        ...otherActions } = actionState;

      return { ...otherActions };
    },
  });
}
function withCanSwimIfNotBlocked(type, state) {
  state.isSwimming = false;

  return Object.assign(type, {
    startSwimming() {
      if (!state.isInAction) {

        state.isInAction = true;
        state.isSwimming = true;

        console.log({ startSwimming: { state } })
      }
    },
    stopSwimming() {
      if (state.isSwimming) {

        state.isInAction = false;
        state.isSwimming = false;

        console.log({ stopSwimming: { state } })
      }
    },
  });
}
function withCanFlyIfNotBlocked(type, state) {
  state.isFlying = false;

  return Object.assign(type, {
    startFlying() {
      if (!state.isInAction) {

        state.isInAction = true;
        state.isFlying = true;

        console.log({ startFlying: { state } })
      }
    },
    stopFlying() {
      if (state.isFlying) {

        state.isInAction = false;
        state.isFlying = false;

        console.log({ stopFlying: { state } })
      }
    },
  });
}
function withLaysEggsIfNotBlocked(type, state) {
  state.isLayingEggs = false;

  return Object.assign(type, {
    startLayingEggs() {
      if (!state.isInAction) {

        state.isInAction = true;
        state.isLayingEggs = true;

        console.log({ startLayingEggs: { state } })
      }
    },
    stopLayingEggs() {
      if (state.isLayingEggs) {

        state.isInAction = false;
        state.isLayingEggs = false;

        console.log({ stopLayingEggs: { state } })
      }
    },
  });
}

function createSeabird(type) {
  const birdState = {
    type,
    actions: {},
  };
  const birdType = {
    valueOf() {
      return JSON.parse(
        JSON.stringify(birdState)
      );
    },
  };
  const { actions } = birdState;

  withActionControl(birdType, actions)
  withLaysEggsIfNotBlocked(birdType, actions);
  withCanFlyIfNotBlocked(birdType, actions);
  withCanSwimIfNotBlocked(birdType, actions);

  return birdType;
}

const wisdom = createSeabird({
  family: 'Albatross',
  genus: 'North Pacific albatross',
  species: 'Laysan albatross',
  name: 'Wisdom',
  sex: 'female',
  age: 70,
});
console.log({ wisdom });

console.log('wisdom.valueOf() ...', wisdom.valueOf());
console.log('wisdom.monitorActions() ...', wisdom.monitorActions());

console.log('wisdom.startFlying();')
wisdom.startFlying();
console.log('wisdom.startFlying();')
wisdom.startFlying();

console.log('wisdom.startSwimming();')
wisdom.startSwimming();
console.log('wisdom.startLayingEggs();')
wisdom.startLayingEggs();

console.log('wisdom.stopFlying();')
wisdom.stopFlying();
console.log('wisdom.stopFlying();')
wisdom.stopFlying();

console.log('wisdom.startSwimming();')
wisdom.startSwimming();
console.log('wisdom.startSwimming();')
wisdom.startSwimming();

console.log('wisdom.startLayingEggs();')
wisdom.startLayingEggs();
console.log('wisdom.startFlying();')
wisdom.startFlying();

console.log('wisdom.stopSwimming();')
wisdom.stopSwimming();
console.log('wisdom.stopSwimming();')
wisdom.stopSwimming();

console.log('wisdom.startLayingEggs();')
wisdom.startLayingEggs();
console.log('wisdom.startLayingEggs();')
wisdom.startLayingEggs();

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

Close with one of the above initial comments ...

"2/2 ... Just take advantage of the language's flexibility and expressiveness. Just be aware of the advantages, pitfalls and comprehensibility (to others) of your modeling approach(es). And once this is checked don't worry about [too strict]* Crockford disciples (or any other school / religion / cult). A good teacher shows you a [path]* and allows / encourages you to discover or follow your own, once you understood what the base/basics are good for."

Peter Seliger
  • 11,747
  • 3
  • 28
  • 37
  • 1/2 Thank you very much! I was thinking about writing composable pieces of code like `const withIncrement = state => ({ setValue() { state.a += 1 } })`. So mutable state is passed in from outside and it can be mutated by methods. This would be along the lines of your example but with a difference that this doesn't actually attach those methods to some "type". It just them and then it's somebody else's job to do it. Real constructor would then call `withIncrement(state)` and assemble methods in final response (something like: `return { ...withIncrement(state) }`). – Zeljko Brckovic Nov 19 '21 at 18:33
  • 2/2 The only thing that I wasn't sure about is the fact that I am writing a function which modifies its parameter. `withIncrement` here is such function. The one who passes in the argument must know that. But if it's a function whose main purpose is to be called in a constructor, I guess it's not dangerous or confusing. – Zeljko Brckovic Nov 19 '21 at 18:36
  • @ZeljkoBrckovic ... And what is the question? You also either might edit the original post accordingly (as addendum) or open a new question where you discuss the new matter (with example code again) based on the latest learnings/progress. – Peter Seliger Nov 19 '21 at 19:02
  • It is not a question. If the purpose of comment is to ask a question you can ignore what I wrote. It was just "thanks" and a comment on some related considerations I had. Never mind – Zeljko Brckovic Nov 19 '21 at 19:15
  • @ZeljkoBrckovic ... Thanks for clarifying. But did the provided answer give you some new insights? Are there any questions left about the above answer's approach? – Peter Seliger Nov 19 '21 at 19:19
  • 1
    Yes. The pattern you showed retains encapsulation while still providing state sharing. But it shows that we must retain a clear distinction between mixins and constructors/factories. Former ones mutate their argument (passed state) and are only to be used in compositions inside constructors/factories, not for making final instances, while latter ones are full fledged instance-creating functions to be exposed for public use. Crockford's examples seemed to show that in his pattern there is no such distinction – Zeljko Brckovic Nov 19 '21 at 20:01
  • @ZeljkoBrckovic ... perfect summarize. – Peter Seliger Nov 19 '21 at 20:05