Search code examples
javascriptnode.jsfunctioncomposition

Composition in JS


I am learning the concepts of Composition in JS. Below is my demo code.

The moveBy function assigns the values correctly to x and y.

However, the setFillColor function does not assign the passed value to fillColor.

What exactly is happening when the setFillColor function is called?

const withMoveBy = (shape) => ({
  moveBy: (diffX, diffY) => {
    shape.dimensions.x += diffX;
    shape.dimensions.y += diffY;
  },
});

const withSetFillColor = (shape) => ({
  setFillColor: (color) => {
    console.log(shape.fillColor);                      // 1
    shape.fillColor = color;
    shape.dimensions.fillColor = color;
    console.log(shape.fillColor);                      // 2
  },
});

const shapeRectangle = (dimensions) => ({
  type: 'rectangle',
  fillColor: 'white',
  dimensions,
});

const shapeCircle = (dimensions) => ({
  type: 'circle',
  fillColor: 'white',
  dimensions,
});

const createShape = (type, dimensions) => {
  let shape = null;
  switch (type) {
    case 'rectangle': {
      shape = shapeRectangle(dimensions);
      break;
    }
    case 'circle': {
      shape = shapeCircle(dimensions);
      break;
    }
  }

  if (shape) {
    shape = {
      ...shape,
      ...withSetFillColor(shape),
      ...withMoveBy(shape),
    };
  }
  return shape;
};

let r = createShape('rectangle', {
  x: 1,
  y: 1,
  width: 10,
  height: 10,
});

let c = createShape('circle', { x: 10, y: 10, diameter: 10 });

r.moveBy(2, 3);
c.moveBy(1, 2);

r.setFillColor('red');
c.setFillColor('blue');

console.log(r);
console.log(c);

OUTPUT:

Line marked as // 1 prints white in case of rectangle as well as circle.

Line marked as // 2 prints red for rectangle and blue for circle.

The final output is:

{
  "type": "rectangle",
  "fillColor": "white",
  "dimensions": {
    "x": 3,
    "y": 4,
    "width": 10,
    "height": 10,
    "fillColor": "red"
  }
}
{
  "type": "circle",
  "fillColor": "white",
  "dimensions": {
    "x": 11,
    "y": 12,
    "diameter": 10,
    "fillColor": "blue"
  }
}

The fillColor as the property of object is still white. However, the one inside of dimensions has taken the correct value.


Solution

  • The problem stems from this assignment in createShape - annotations by me:

        // creating the "new object"
        shape = {
          ...shape, // shallow copying of the "old object"
          ...withSetFillColor(shape),
          ...withMoveBy(shape),
        };
    

    Here, you create a new object that is composed of:

    • shallow-copied properties of the existing ...shape (type, fillcolor, dimensions which is an object)
    • setFillColor, a closure that is bound to shape (the old object)
    • moveBy, a closure that is bound to shape (the old object)

    After this statement is executed, you have created two shapes:

    • The "old object", which the methods operate on
    • The "new object", which you return

    Out of the properties copied from the old object, only dimensions is a non-primitive value, so it is shared between the instances.

    Then, when you call:

    r.moveBy(2, 3);
    

    it changes oldShape.dimensions, but it's the same object as newShape.dimensions, so it's visible in the output.

    However, this call:

    r.setFillColor('red');
    

    modifies the fillColor property of the oldShape, which you are not seeing. It also writes to oldShape.dimensions.fillColor, which, again, is shared between the objects, so that change is visible in both.