Search code examples
javascriptfunctionobjectfactorymixins

Trying to understand an object composition pattern which features a factory and a function based mixin technique


I'm trying to understand behavior of function based composition in JavaScript.

const Animal = (name) => {
  let properties = { name };
  return ({
    get name() { return properties.name },
    set name(newName) { properties.name = newName },
    breathe: function() {console.log(`${this.name} breathes!`); }
  })
}

const aquaticKind = (animal) => ({
  swim: () => console.log(`${animal.name} swims`)
})

const walkingKind = (animal, noOfLegs) => {
  const properties = { noOfLegs }
  return ({
    get noOfLegs() { return properties.noOfLegs },
    set noOfLegs(n) { properties.noOfLegs = n; },
    walk: () => console.log(`${animal.name} walks with ${properties.noOfLegs} legs`)
  })
}

const egglayingKind = (animal) => ({
  layEgg: () => console.log(`${animal.name} laid an egg`)
})

const Crocodile = (name) => {
  const info = Animal(name);
  return Object.assign(info,
                       walkingKind(info, 4),
                       aquaticKind(info),
                       egglayingKind(info)
                      );
}
const snooty = Crocodile('snooty');
snooty.breathe();
snooty.swim();
snooty.walk();
snooty.name = "coolie";
snooty.noOfLegs = 23 // I expected this to get update to 23
snooty.swim();
snooty.walk();
snooty.layEgg();

If you run the code above, you will see that noOfLegs never get updated, while name get updated. I can't seem to wrap my head around this. How do we make noOfLegs get updated too?


Solution

  • MDN Documentation for object.assign shows you how to copy "accessors"

    Here's your code that works as expected - the completeAssign function is based entirely on the code in that link

    const completeAssign = (target, ...sources) => {
        sources.forEach(source => {
            const descriptors = Object.keys(source).reduce((descriptors, key) => {
                descriptors[key] = Object.getOwnPropertyDescriptor(source, key);
                return descriptors;
            }, {});
            Object.getOwnPropertySymbols(source).forEach(sym => {
                const descriptor = Object.getOwnPropertyDescriptor(source, sym);
                if (descriptor.enumerable) {
                    descriptors[sym] = descriptor;
                }
            });
            Object.defineProperties(target, descriptors);
        });
        return target;
    };
    
    const Animal = (name) => {
        const properties = { name };
        return ({
            get name() { return properties.name },
            set name(newName) { properties.name = newName },
            breathe () { console.log(`${this.name} breathes!`); }
        })
    }
    
    const aquaticKind = (animal) => ({ 
        swim: () => console.log(`${animal.name} swims`) 
    });
    
    const walkingKind = (animal, noOfLegs) => {
        const properties = { noOfLegs };
        return ({
            get noOfLegs() { return properties.noOfLegs },
            set noOfLegs(n) { properties.noOfLegs = n; },
            walk: () => console.log(`${animal.name} walks with ${properties.noOfLegs} legs`)
        })
    }
    
    const egglayingKind = (animal) => ({
        layEgg: () => console.log(`${animal.name} laid an egg`)
    })
    
    const Crocodile = (name) => {
        const info = Animal(name);
        return completeAssign(info,
            walkingKind(info, 4),
            aquaticKind(info),
            egglayingKind(info)
        );
    }
    const snooty = Crocodile('snooty');
    snooty.breathe();
    snooty.swim();
    snooty.walk();
    snooty.name = "coolie";
    snooty.noOfLegs = 23;
    snooty.swim();
    snooty.walk();
    snooty.layEgg();