Skip to content

Abstracting JavaScript

tdkehoe edited this page Aug 19, 2015 · 1 revision

How much abstraction is the right amount? Should you use nested for loops, single for loops, or not use for loops?

The following Jasmine spec.js tests if an array of arrays with an index has been converted to an object of objects with the index as the keys. (The spec.js files goes into a directory spec. The directory spec goes into the same folder as houses.js. Run jasmine init and then jasmine to test the code.)

var code = require('./../houses.js');

var houses = [ [ 'id', 'year', 'city', 'bedrooms' ],
               [ '3148du38', 1992, 'Boulder', 2 ],
               [ '4d9g72lg', 1906, 'Lafayette', 4 ],
               [ '9fgf7e24', 1976, 'Longmont', 3 ] ];

describe('Test houses', function() {
  it('Converts array of arrays with an index to an object of objects with the index as keys', function() {
    expect(code.realEstate(houses)).toEqual(
      { '3148du38': { year: 1992, city: 'Boulder', bedrooms: '2' },
        '4d9g72lg': { year: 1906, city: 'Lafayette', bedrooms: '4' },
        '9fgf7e24': { year: 1976, city: 'Longmont', bedrooms: '3' } });
  });
});

Make a module for testing. Include the input and output so you can visually compare them. Save this as houses.js.

var houses = [ [ 'id', 'year', 'city', 'bedrooms' ],
               [ '3148du38', 1992, 'Boulder', 2 ],
               [ '4d9g72lg', 1906, 'Lafayette', 4 ],
               [ '9fgf7e24', 1976, 'Longmont', 3 ] ];

var output =
  { '3148du38': { year: 1992, city: 'Boulder', bedrooms: '2' },
    '4d9g72lg': { year: 1906, city: 'Lafayette', bedrooms: '4' },
    '9fgf7e24': { year: 1976, city: 'Longmont', bedrooms: '3' } };

module.exports = {
  realEstate: function(houses) {

    // code goes here

  }
};

Look at the output. Notice that it consists of four objects: three inner objects contained in an outer object. Start by creating the four objects. Return the outer object to the Jasmine test.

var houses = [ [ 'id', 'year', 'city', 'bedrooms' ],
               [ '3148du38', 1992, 'Boulder', 2 ],
               [ '4d9g72lg', 1906, 'Lafayette', 4 ],
               [ '9fgf7e24', 1976, 'Longmont', 3 ] ];

var output =
  { '3148du38': { year: 1992, city: 'Boulder', bedrooms: '2' },
    '4d9g72lg': { year: 1906, city: 'Lafayette', bedrooms: '4' },
    '9fgf7e24': { year: 1976, city: 'Longmont', bedrooms: '3' } };

module.exports = {
  realEstate: function(houses) {

    var house1 = {};
    var house2 = {};
    var house3 = {};
    var outerObject = {};

    return outerObject;
  }
};

Now play around with accessing each element in the arrays:

console.log(houses[0][1]);

console.log(houses[1][0]);

console.log(houses[1][1]);

Play around with this until you're comfortable accessing any element in the arrays. Then build the first house:

var houses = [ [ 'id', 'year', 'city', 'bedrooms' ],
               [ '3148du38', 1992, 'Boulder', 2 ],
               [ '4d9g72lg', 1906, 'Lafayette', 4 ],
               [ '9fgf7e24', 1976, 'Longmont', 3 ] ];

var output =
  { '3148du38': { year: 1992, city: 'Boulder', bedrooms: '2' },
    '4d9g72lg': { year: 1906, city: 'Lafayette', bedrooms: '4' },
    '9fgf7e24': { year: 1976, city: 'Longmont', bedrooms: '3' } };

module.exports = {
  realEstate: function(houses) {

    var house1 = {};
    var house2 = {};
    var house3 = {};
    var outerObject = {};

    house1[houses[0][1]] = houses[1][1]; // Object {year: 1992}
    house1[houses[0][2]] = houses[1][2]; // Object {year: 1992, city: "Boulder"}
    house1[houses[0][3]] = houses[1][3]; // Object {year: 1992, city: "Boulder", bedrooms: 2}

    console.log(house1);

    return outerObject;
  }
};

Next build the other two houses.

var houses = [ [ 'id', 'year', 'city', 'bedrooms' ],
               [ '3148du38', 1992, 'Boulder', 2 ],
               [ '4d9g72lg', 1906, 'Lafayette', 4 ],
               [ '9fgf7e24', 1976, 'Longmont', 3 ] ];

var output =
  { '3148du38': { year: 1992, city: 'Boulder', bedrooms: '2' },
    '4d9g72lg': { year: 1906, city: 'Lafayette', bedrooms: '4' },
    '9fgf7e24': { year: 1976, city: 'Longmont', bedrooms: '3' } };

module.exports = {
  realEstate: function(houses) {

    var house1 = {};
    var house2 = {};
    var house3 = {};
    var outerObject = {};

    house1[houses[0][1]] = houses[1][1]; // Object {year: 1992}
    house1[houses[0][2]] = houses[1][2]; // Object {year: 1992, city: "Boulder"}
    house1[houses[0][3]] = houses[1][3]; // Object {year: 1992, city: "Boulder", bedrooms: 2}

    house2[houses[0][1]] = houses[2][1]; // Object {year: 1906}
    house2[houses[0][2]] = houses[2][2]; // Object {year: 1906, city: "Lafayette"}
    house2[houses[0][3]] = houses[2][3]; // Object {year: 1906, city: "Lafayette", bedrooms: 4}

    house3[houses[0][1]] = houses[3][1]; // Object {year: 1976}
    house3[houses[0][2]] = houses[3][2]; // Object {year: 1976, city: "Longmont"}
    house3[houses[0][3]] = houses[3][3]; // Object {year: 1976, city: "Longmont", bedrooms: 3}

    return outerObject;
  }
};

Now put the three houses into the outer array, with the index keys.

var houses = [ [ 'id', 'year', 'city', 'bedrooms' ],
               [ '3148du38', 1992, 'Boulder', 2 ],
               [ '4d9g72lg', 1906, 'Lafayette', 4 ],
               [ '9fgf7e24', 1976, 'Longmont', 3 ] ];

var output =
  { '3148du38': { year: 1992, city: 'Boulder', bedrooms: '2' },
    '4d9g72lg': { year: 1906, city: 'Lafayette', bedrooms: '4' },
    '9fgf7e24': { year: 1976, city: 'Longmont', bedrooms: '3' } };

module.exports = {
  realEstate: function(houses) {

    var house1 = {};
    var house2 = {};
    var house3 = {};
    var outerObject = {};

    house1[houses[0][1]] = houses[1][1]; // Object {year: 1992}
    house1[houses[0][2]] = houses[1][2]; // Object {year: 1992, city: "Boulder"}
    house1[houses[0][3]] = houses[1][3]; // Object {year: 1992, city: "Boulder", bedrooms: 2}

    house2[houses[0][1]] = houses[2][1]; // Object {year: 1906}
    house2[houses[0][2]] = houses[2][2]; // Object {year: 1906, city: "Lafayette"}
    house2[houses[0][3]] = houses[2][3]; // Object {year: 1906, city: "Lafayette", bedrooms: 4}

    house3[houses[0][1]] = houses[3][1]; // Object {year: 1976}
    house3[houses[0][2]] = houses[3][2]; // Object {year: 1976, city: "Longmont"}
    house3[houses[0][3]] = houses[3][3]; // Object {year: 1976, city: "Longmont", bedrooms: 3}

    outerObject[houses[1][0]] = house1; // Object {3148du38: Object}
    outerObject[houses[2][0]] = house2; // Object {3148du38: Object, 4d9g72lg: Object}
    outerObject[houses[3][0]] = house3; // Object {3148du38: Object, 4d9g72lg: Object, 9fgf7e24: Object}

    return outerObject;           // Object {3148du38: Object, 4d9g72lg: Object, 9fgf7e24: Object}
  }
};

Run the Jasmine test. It fails because the arrays have bedrooms as a number but the objects have bedrooms as a string. Convert the number into a string.

var houses = [ [ 'id', 'year', 'city', 'bedrooms' ],
               [ '3148du38', 1992, 'Boulder', 2 ],
               [ '4d9g72lg', 1906, 'Lafayette', 4 ],
               [ '9fgf7e24', 1976, 'Longmont', 3 ] ];

var output =
  { '3148du38': { year: 1992, city: 'Boulder', bedrooms: '2' },
    '4d9g72lg': { year: 1906, city: 'Lafayette', bedrooms: '4' },
    '9fgf7e24': { year: 1976, city: 'Longmont', bedrooms: '3' } };

module.exports = {
  realEstate: function(houses) {

    var house1 = {};
    var house2 = {};
    var house3 = {};
    var outerObject = {};

    houses[1][3] = houses[1][3].toString();
    houses[2][3] = houses[2][3].toString();
    houses[3][3] = houses[3][3].toString();

    house1[houses[0][1]] = houses[1][1]; // Object {year: 1992}
    house1[houses[0][2]] = houses[1][2]; // Object {year: 1992, city: "Boulder"}
    house1[houses[0][3]] = houses[1][3]; // Object {year: 1992, city: "Boulder", bedrooms: 2}

    house2[houses[0][1]] = houses[2][1]; // Object {year: 1906}
    house2[houses[0][2]] = houses[2][2]; // Object {year: 1906, city: "Lafayette"}
    house2[houses[0][3]] = houses[2][3]; // Object {year: 1906, city: "Lafayette", bedrooms: 4}

    house3[houses[0][1]] = houses[3][1]; // Object {year: 1976}
    house3[houses[0][2]] = houses[3][2]; // Object {year: 1976, city: "Longmont"}
    house3[houses[0][3]] = houses[3][3]; // Object {year: 1976, city: "Longmont", bedrooms: 3}

    outerObject[houses[1][0]] = house1; // Object {3148du38: Object}
    outerObject[houses[2][0]] = house2; // Object {3148du38: Object, 4d9g72lg: Object}
    outerObject[houses[3][0]] = house3; // Object {3148du38: Object, 4d9g72lg: Object, 9fgf7e24: Object}

    return outerObject;           // Object {3148du38: Object, 4d9g72lg: Object, 9fgf7e24: Object}
  }
};

houses.js should now pass the Jasmine test.

Consider how to abstract your code. What changes? What stays the same? You can't use forEach loops because you start at element 1, when forEach loops always start at element 0. You'll have to use for loops.

var houses = [ [ 'id', 'year', 'city', 'bedrooms' ],
  [ '3148du38', 1992, 'Boulder', 2 ],
  [ '4d9g72lg', 1906, 'Lafayette', 4 ],
  [ '9fgf7e24', 1976, 'Longmont', 3 ] ];

module.exports = {
  realEstate: function(houses) {

    var house1 = {};
    var house2 = {};
    var house3 = {};
    var outerObject = {};

    for (var i = 1; i < houses.length; i++) {
      houses[i][3] = houses[i][3].toString();
    }

    for (var i = 1; i < houses[0].length; i++) {
      house1[houses[0][i]] = houses[1][i];
    }

    for (var i = 1; i < houses[0].length; i++) {
      house2[houses[0][i]] = houses[2][i];
    }

    for (var i = 1; i < houses[0].length; i++) {
      house3[houses[0][i]] = houses[3][i];
    }

    outerObject[houses[1][0]] = house1;
    outerObject[houses[2][0]] = house2;
    outerObject[houses[3][0]] = house3;

    return outerObject;
  }
};

Save this code as houses2.js. Change line 1 of spec.js to

var code = require('./../houses2.js');

Run the Jasmine test. It should pass.

Now consider how to abstract this block into a for loop:

outerObject[houses[1][0]] = house1;
outerObject[houses[2][0]] = house2;
outerObject[houses[3][0]] = house3;

This doesn't work:

for (var i = 1; i < houses.length; i++) {
  outerObject[houses[i][0]] = "house" + i;
}

Instead think about how to make a for loop that, on each pass, builds a house and inserts the new house into outerObject.

var houses = [ [ 'id', 'year', 'city', 'bedrooms' ],
  [ '3148du38', 1992, 'Boulder', 2 ],
  [ '4d9g72lg', 1906, 'Lafayette', 4 ],
  [ '9fgf7e24', 1976, 'Longmont', 3 ] ];

module.exports = {
  realEstate: function(houses) {
    var outerObject = {};
    for (var i = 1; i < houses.length; i++) {
      houses[i][3] = houses[i][3].toString();
      var house = {};
      for (var j = 1; j < houses[0].length; j++) {
        house[houses[0][j]] = houses[1][j];
      }
      outerObject[houses[i][0]] = house;
    }
    return outerObject;
  }
};

Save this code as houses3.js and change the first line of spec.js to match. Run the test and see that it fails. It builds the same house three times, instead of building three different houses.

Instead of staring at houses3.js trying to figure out two levels of abstraction, go back to house.js and make it produce the same error.

var houses = [ [ 'id', 'year', 'city', 'bedrooms' ],
               [ '3148du38', 1992, 'Boulder', 2 ],
               [ '4d9g72lg', 1906, 'Lafayette', 4 ],
               [ '9fgf7e24', 1976, 'Longmont', 3 ] ];

var output =
  { '3148du38': { year: 1992, city: 'Boulder', bedrooms: '2' },
    '4d9g72lg': { year: 1906, city: 'Lafayette', bedrooms: '4' },
    '9fgf7e24': { year: 1976, city: 'Longmont', bedrooms: '3' } };

module.exports = {
  realEstate: function(houses) {

    var house1 = {};
    var house2 = {};
    var house3 = {};
    var outerObject = {};

    houses[1][3] = houses[1][3].toString();
    houses[2][3] = houses[2][3].toString();
    houses[3][3] = houses[3][3].toString();

    house1[houses[0][1]] = houses[1][1]; // Object {year: 1992}
    house1[houses[0][2]] = houses[1][2]; // Object {year: 1992, city: "Boulder"}
    house1[houses[0][3]] = houses[1][3]; // Object {year: 1992, city: "Boulder", bedrooms: 2}

    house1[houses[0][1]] = houses[1][1]; // Object {year: 1992}
    house1[houses[0][2]] = houses[1][2]; // Object {year: 1992, city: "Boulder"}
    house1[houses[0][3]] = houses[1][3]; // Object {year: 1992, city: "Boulder", bedrooms: 2}

    house1[houses[0][1]] = houses[1][1]; // Object {year: 1992}
    house1[houses[0][2]] = houses[1][2]; // Object {year: 1992, city: "Boulder"}
    house1[houses[0][3]] = houses[1][3]; // Object {year: 1992, city: "Boulder", bedrooms: 2}

    outerObject[houses[1][0]] = house1; // Object {3148du38: Object}
    outerObject[houses[2][0]] = house2; // Object {3148du38: Object, 4d9g72lg: Object}
    outerObject[houses[3][0]] = house3; // Object {3148du38: Object, 4d9g72lg: Object, 9fgf7e24: Object}

    return outerObject;           // Object {3148du38: Object, 3148du38: Object, 3148du38: Object}
  }
};

Now it should be clear what needs to be fixed in house3.js:

house[houses[0][j]] = houses[i][j];

The finished code should pass the Jasmine test:

var houses = [ [ 'id', 'year', 'city', 'bedrooms' ],
  [ '3148du38', 1992, 'Boulder', 2 ],
  [ '4d9g72lg', 1906, 'Lafayette', 4 ],
  [ '9fgf7e24', 1976, 'Longmont', 3 ] ];

module.exports = {
  realEstate: function(houses) {
    var outerObject = {};
    for (var i = 1; i < houses.length; i++) {
      houses[i][3] = houses[i][3].toString();
      var house = {};
      for (var j = 1; j < houses[0].length; j++) {
        house[houses[0][j]] = houses[i][j];
      }
      outerObject[houses[i][0]] = house;
    }
    return outerObject;
  }
};

Lessons:

  • Start without abstractions. Build the solution one element at a time, without for loops.
  • Add abstractions such as for loops when you understand what they will do.
  • Fix errors by going back to the non-abstract code.
Clone this wiki locally