skip to Main Content

I have a main class named Rule with a param attribute and several subclasses as example:

    class Rule {
      param = {}
      constructor (param) {
        // here I want to add the logic to fuse this.param from child component (all are different) and param provided to the constructor
      }
    }

    class SubRule1 extends Rule {
      param = {
        subparam: {
          values: ['A', 'B', 'C'],
          value: 'A'
        }
      }
      constructor (param) {
        super(param)
      }
    }
    
    class SubRule2 extends Rule {
      param = {
        subparam: {
          values: [1, 2],
          value: 1
        }
      }
      constructor (param) {
        super(param)
      }
    }

    let myrule = new SubRule1({ subparam: 'B' })
    >> expected Object { subparam: { values: ['A', 'B', 'C'], value: 'B' }}
    let myrule2 = new SubRule2({ subparam: 2 })
    >> expected Object { subparam: { values: [1, 2], value: 2 }}

When instantiate a new object, I would like to fuse this.param and the param parameter given to the constructor for all the subclasses. I wanted to add the logic in the Rule class, however, when accessing this.param in the Rule.constructor class I do not access the child this.param attribute but the one defined in Rule.
What would be the right design to properly initiate this.param in all subclasses ?

3

Answers


  1. To access the param parameter from parent class constructor, you should not use this.param, instead just use console.log(param) which is passed as parameter to the contructor of parent class. this.param is a pointer to the variable of that class only.

        class Rule {
          param = {}
          constructor (param) {
            console.log(param)
          }
        }
    
        class SubRule extends Rule {
          param = {
            subparam1: 'test'
          }
          constructor (param) {
            super(param)
          }
        }
        
        let myrule = new SubRule()
    
        // Object {  } instead of Object { subparam1: 'test' }

    Something like this may work for you.

    Login or Signup to reply.
  2. The pattern you’re looking for is

    actual_params = Object.assign({}, defaults, user_options)
    

    To make this work with inheritance, defaults must be static:

    class Whatever {
        constructor(options = {}) {
            this.config = Object.assign({}, this.constructor.defaults, options)
        }
    }
    
    class Deriv extends Whatever {
        static defaults = {'foo': 'bar', 'spam': 'pam'}
    }
    
    let a = new Deriv()
    console.log(a.config)
    
    let b = new Deriv({'spam': 'hello'})
    console.log(b.config)

    If you want to merge them deeply, this is another, more complicated story.

    Login or Signup to reply.
  3. From my above comments …

    "The latest changes which did target all param data structures do open an entirely new topic which is described best with … "How to deeply merge two structurally similar objects without loosing data by partial overwrites?" The original question about the best class / constructor / super design gets marginalized in comparison to this silent topic shift."

    "@user1595929 … The OP might have a look into one answer of following question … "Comparing 2 nested data-structures,target+source,what are appropriate merge-strategies for missing target values compared to their source counterpart?" … in order to get some inspiration about deep merging approaches."

    "@user1595929 … in addition both new examples in terms of each of the expected results are broken/wrong … in order to meet the expectations the example codes need to be … new SubRule1({ subparam: { value: 'B' } }) instead of new SubRule1({ subparam: 'B' }) and new SubRule2({ subparam: { value: 2 } }) instead of new SubRule2({ subparam: 2 })"

    (1) First things first … regarding the code comment of the OP’s Rule constructor …

    // here I want to add the logic to fuse this.param from child component (all are different) and param provided to the constructor

    … a parent- or super-class has to be oblivious (thus it does not make any assumptions) about child / sub-class properties and operations.

    (2) In addition, regarding just the handling of each of the Sub/Rules param member handling, no inheritance, hence no RuleSubRule (parent-child) relationship is even needed. This is due to how at construction time the initialization of each instance’s this.param gets handled.

    Even though as with the OP’s example code both (sub-)types SubRule1 and SubRule2 each extend the (super-)type Rule the this.param initialization is covered/shadowed entirely by each of the types itself. Whatever value this.param was, at the end of a super call (chain), at the latest with any sub-type constructor this value gets entirely reassigned/overwritten (like demonstrated with the OP’s params defaults of SubRule1 and SubRule2). Thus, if it was not for any other prototypal functionality which hasn’t been shown yet, any Rule related inheritance is totally unnecessary.

    (3) Moreover, the OP with one of the more recent edits introduced a far more complex merge behavior for param objects than one could have easily achieved with non nested objects and just e.g. …

    Object.assign(this.param, param);
    

    Instead the expected merger of …

    {
      subparam: {
        values: ['A', 'B', 'C'],
        value: 'A',
      }
    }
    

    … and of …

    {
      subparam: {
        value: 'B',
      }
    }
    

    … is …

    {
      subparam: {
        values: ['A', 'B', 'C'],
        value: 'B',
      }
    }
    

    … which asks for a custom merge implementation.

    Summary The OP’s main task is rather implementing a suitable custom merge strategy than making use of sub-class/type based inheritance since there most probably might not even be a valid reason for the latter.

    The next provided example code proves the above said (though it keeps the inheritance feature, in order to stay with the OP’s main code structure).

    class Rule {
      param = {};
    
      constructor (options = {}) {
        // - a simple `Object.assign` is suitable due
        //   to `this.param` being an empty object.
        Object.assign(this.param, options);
      }
    }
    
    class SubRule1 extends Rule {
      param = {
        subparam: {
          values: ['A', 'B', 'C'],
          value: 'A',
        },
      };
      constructor (options = {}) {
        super(/*no `options` passing needed due to own `param`*/);
        mergeDataStructures(this.param, options);
      }
    }
    class SubRule2 extends Rule {
      param = {
        subparam: {
          values: [1, 2],
          value: 1,
        },
      };
      constructor (options = {}) {
        super(/*no `options` passing needed due to own `param`*/);
        mergeDataStructures(this.param, options);
      }
    }
    const myRule1 = new SubRule1({ subparam: { value: 'B' } });
    const myRule2 = new SubRule2({ subparam: { value: 2 } });
    
    console.log({
      myRule1, // { subparam: { values: ['A', 'B', 'C'], value: 'B' }}
      myRule2, // { subparam: { values: [1, 2], value: 2 }}
    });
    .as-console-wrapper { min-height: 100%!important; top: 0; }
    <script>
    // - a pushing/patching approach of assigning/overwriting
    //   target-values of deeply nested data-structures.
    function mergeDataStructures(target, source) {
    
      const targetIsArray = Array.isArray(target);
      const sourceIsArray = Array.isArray(source);
    
      const targetIsNonArrayObject =
        !!target && 'object' === typeof target && !targetIsArray;
      const sourceIsNonArrayObject =
        !!source && 'object' === typeof source && !sourceIsArray;
    
      if (targetIsArray && sourceIsArray) {
        source
          .forEach((_/*sourceItem*/, idx) =>
            assignValue(target, source, idx)
          );
      } else if (targetIsNonArrayObject && sourceIsNonArrayObject) {
        Object
          .keys(source)
          .forEach(key =>
            assignValue(target, source, key)
          );
      } else {
        target = cloneDataStructure(source);
      }
      return target;
    
      function assignValue(target, source, key) {
        const sourceValue = source[key];
    
        if (!target.hasOwnProperty(key)) {
    
          target[key] = cloneDataStructure(sourceValue);
        } else {
          const targetValue = target[key];
    
          const targetValueIsObject =
            (!!targetValue && 'object' === typeof targetValue);
          const sourceValueIsObject =
            (!!sourceValue && 'object' === typeof sourceValue);
          const targetValueIsArray =
            targetValueIsObject && Array.isArray(targetValue);
          const sourceValueIsArray =
            sourceValueIsObject && Array.isArray(sourceValue);
    
          if (
            (targetValueIsArray && sourceValueIsArray) ||
            (targetValueIsObject && sourceValueIsObject)
          ) {
            mergeDataStructures(targetValue, sourceValue);
          } else {
            target[key] = cloneDataStructure(sourceValue);
          }
        }
      }
    }
    const cloneDataStructure =
      ('function' === typeof structuredClone)
      && structuredClone
      || (value => JSON.parse(JSON.stringify(value)));
    </script>

    Edit

    In case the OP wants to go for full inheritance with parameter passing/forwarding via super calls, the approach needs to be changed from defining each type’s default param as own property.

    This is due to this (hence this.param) is not allowed to be accessed before or during a super call. Thus each type’s default param configuration has to be stored as e.g. local variable within each type’s (or all type’s) module scope. I would not declare such configurations as static class properties for they are exposed then, whereas a module scope ensures the protection of each default param config.

    The before provided above example code which actually does not need any inheritance would change then to a real inheritance approach where the implementation might be similar to the next provided code …

    const subRule2Param = {
      subparam: {
        values: [1, 2],
        value: 1,
      },
    };
    const subRule1Param = {
      subparam: {
        values: ['A', 'B', 'C'],
        value: 'A',
      },
    };
    const baseRuleParam = { subparam: { value: 'base' } };
    
    class Rule {
      param = {};
    
      constructor (options = {}) {
        // - a simple `Object.assign` is suitable due
        //   to `this.param` being an empty object.
        Object.assign(
          this.param,
          mergeDataStructures(
            cloneDataStructure(baseRuleParam),
            options
          )
        );
      }
    }
    
    class SubRule1 extends Rule {
      constructor (options = {}) {
        super(
          mergeDataStructures(
            cloneDataStructure(subRule1Param),
            options
          )
        );
      }
    }
    class SubRule2 extends Rule {
      constructor (options = {}) {
        super(
          mergeDataStructures(
            cloneDataStructure(subRule2Param),
            options
          )
        );
      }
    }
    const myRule1 = new SubRule1({ subparam: { value: 'B' } });
    const myRule2 = new SubRule2({ subparam: { value: 2 } });
    
    console.log({
      myRule1, // { subparam: { values: ['A', 'B', 'C'], value: 'B' }}
      myRule2, // { subparam: { values: [1, 2], value: 2 }}
    });
    .as-console-wrapper { min-height: 100%!important; top: 0; }
    <script>
    // - a pushing/patching approach of assigning/overwriting
    //   target-values of deeply nested data-structures.
    function mergeDataStructures(target, source) {
    
      const targetIsArray = Array.isArray(target);
      const sourceIsArray = Array.isArray(source);
    
      const targetIsNonArrayObject =
        !!target && 'object' === typeof target && !targetIsArray;
      const sourceIsNonArrayObject =
        !!source && 'object' === typeof source && !sourceIsArray;
    
      if (targetIsArray && sourceIsArray) {
        source
          .forEach((_/*sourceItem*/, idx) =>
            assignValue(target, source, idx)
          );
      } else if (targetIsNonArrayObject && sourceIsNonArrayObject) {
        Object
          .keys(source)
          .forEach(key =>
            assignValue(target, source, key)
          );
      } else {
        target = cloneDataStructure(source);
      }
      return target;
    
      function assignValue(target, source, key) {
        const sourceValue = source[key];
    
        if (!target.hasOwnProperty(key)) {
    
          target[key] = cloneDataStructure(sourceValue);
        } else {
          const targetValue = target[key];
    
          const targetValueIsObject =
            (!!targetValue && 'object' === typeof targetValue);
          const sourceValueIsObject =
            (!!sourceValue && 'object' === typeof sourceValue);
          const targetValueIsArray =
            targetValueIsObject && Array.isArray(targetValue);
          const sourceValueIsArray =
            sourceValueIsObject && Array.isArray(sourceValue);
    
          if (
            (targetValueIsArray && sourceValueIsArray) ||
            (targetValueIsObject && sourceValueIsObject)
          ) {
            mergeDataStructures(targetValue, sourceValue);
          } else {
            target[key] = cloneDataStructure(sourceValue);
          }
        }
      }
    }
    const cloneDataStructure =
      ('function' === typeof structuredClone)
      && structuredClone
      || (value => JSON.parse(JSON.stringify(value)));
    </script>

    Edit due to a user’s comment …

    "Your mergeDataStructures implementation is needlessly complex for the specific usecase of the OP. Just do the simplest possible thing." – Adam

    ""… is needlessly complex for the specific usecase of the OP"_ … how does one know? The two provided solutions (with and whithout parameter passing to super) together with each of its explanation are answering the OP’s question about the OP’s class design/concept. And the custom merge implementation got provided in addition for it will cover merging complex nested data structures better than e.g. underscore/lodash merge."_ – Peter Seliger

    One also could make use of the _.merge and _.cloneDeep methods of either libraries Lodash or Underscore.js.

    const subRule2Param = {
      subparam: {
        values: [1, 2],
        value: 1,
      },
    };
    const subRule1Param = {
      subparam: {
        values: ['A', 'B', 'C'],
        value: 'A',
      },
    };
    const baseRuleParam = { subparam: { value: 'base' } };
    
    class Rule {
      param = {};
    
      constructor (options = {}) {
        // - a simple `Object.assign` is suitable due
        //   to `this.param` being an empty object.
        Object.assign(
          this.param,
          _.merge(
            _.cloneDeep(baseRuleParam),
            options
          )
        );
      }
    }
    
    class SubRule1 extends Rule {
      constructor (options = {}) {
        super(
          _.merge(
            _.cloneDeep(subRule1Param),
            options
          )
        );
      }
    }
    class SubRule2 extends Rule {
      constructor (options = {}) {
        super(
          _.merge(
            _.cloneDeep(subRule2Param),
            options
          )
        );
      }
    }
    const myRule1 = new SubRule1({ subparam: { value: 'B' } });
    const myRule2 = new SubRule2({ subparam: { value: 2 } });
    
    console.log({
      myRule1, // { subparam: { values: ['A', 'B', 'C'], value: 'B' }}
      myRule2, // { subparam: { values: [1, 2], value: 2 }}
    });
    .as-console-wrapper { min-height: 100%!important; top: 0; }
    <script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.21/lodash.min.js"></script>
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search