5
\$\begingroup\$

This was part of an exercise from exercism.io. If you fancy, view the repo here.

Given problem statement:

Write a program which produces the lyrics to that beloved classic, that field-trip favorite: 99 Bottles of Beer on the Wall.

  • Remove as much duplication as you possibly can.
  • Optimize for readability, even if it means introducing duplication.
  • If you've removed all the duplication, do you have a lot of conditionals? Try replacing the conditionals with polymorphism, if it applies in this language. How readable is it?

Note: the spec was given as part of the problem.


I'm fairly inexperienced with javascript so I'm wondering if there's anything that can be improved.

I'm especially interested in any critiques of how I implemented polymorphism using the factory pattern.


beer-song.js

class BeerSong {
  sing(...verses) {
    this.validateArguments(verses);
    return this.verses(...verses);
  }

  verses(starting, ending = 0) {
    let verses = '';

    while(starting >= ending) {
      verses += this.verse(starting) + '\n';
      starting--;
    }

    return verses.trim();
  }

  verse(number) {
    const factory = new BottleFactory();
    const bottle = factory.getBottle(number);
    const nextBottle = factory.getBottle(bottle.successor());

    return `` +
      `${bottle.quantity().capitalize()} ${bottle.container()} of beer on the wall, ` +
      `${bottle.quantity()} ${bottle.container()} of beer.\n` +
      `${bottle.action()}, ` +
      `${nextBottle.quantity()} ${nextBottle.container()} of beer on the wall.\n`;
  }

  validateArguments(...args) {
    if (args.length > 2 || args.length < 1) {
      throw "Invalid arguments: max 2.";
    }
    if (args.length == 2 && args[0] < args[1]) {
      throw "Invalid arguments: must be in decreasing order.";
    }
  }
}

class Bottle {
  constructor(number) {
    this.number = number;
  }

  container() {
    return 'bottles';
  }

  quantity() {
    return this.number.toString();
  }

  action() {
    return `Take ${this.pronoun()} down and pass it around`;
  }

  pronoun() {
    return 'one';
  }

  successor() {
    return this.number - 1;
  }
}

class OneBottle extends Bottle {
  constructor() {
    super(1);
  }

  container() {
    return 'bottle';
  }

  pronoun() {
    return 'it';
  }
}

class ZeroBottle extends Bottle {
  constructor() {
    super(0);
  }

  quantity() {
    return 'no more';
  }

  action() {
    return 'Go to the store and buy some more';
  }

  successor() {
    return 99;
  }
}

class BottleFactory {
  getBottle(number) {
    switch (number) {
      case 1:
        return new OneBottle(number);
        break;
      case 0:
        return new ZeroBottle(number);
        break;
      default:
        return new Bottle(number);
    }
  }
}

String.prototype.capitalize = function() {
  return this.charAt(0).toUpperCase() + this.slice(1);
}

module.exports = BeerSong;

beer-song.spec.js

var BeerSong = require('./beer-song');

describe('BeerSong', function() {
  var song = new BeerSong();

  it('prints an arbitrary verse', function() {
    var expected = '8 bottles of beer on the wall, 8 bottles of beer.\nTake one down and pass it around, 7 bottles of beer on the wall.\n';
    expect(song.verse(8)).toEqual(expected);
  });

  it('handles 2 bottles', function() {
    var expected = '2 bottles of beer on the wall, 2 bottles of beer.\nTake one down and pass it around, 1 bottle of beer on the wall.\n';
    expect(song.verse(2)).toEqual(expected);
  });

  it('handles 1 bottle', function() {
    var expected = '1 bottle of beer on the wall, 1 bottle of beer.\nTake it down and pass it around, no more bottles of beer on the wall.\n';
    expect(song.verse(1)).toEqual(expected);
  });

  it('handles 0 bottles', function() {
    var expected = 'No more bottles of beer on the wall, no more bottles of beer.\nGo to the store and buy some more, 99 bottles of beer on the wall.\n';
    expect(song.verse(0)).toEqual(expected);
  });

  it('sings several verses', function() {
    var expected = '8 bottles of beer on the wall, 8 bottles of beer.\nTake one down and pass it around, 7 bottles of beer on the wall.\n\n7 bottles of beer on the wall, 7 bottles of beer.\nTake one down and pass it around, 6 bottles of beer on the wall.\n\n6 bottles of beer on the wall, 6 bottles of beer.\nTake one down and pass it around, 5 bottles of beer on the wall.\n'.trim();
    expect(song.sing(8, 6)).toEqual(expected);
  });

  it('sings the rest of the verses', function() {
    var expected = '3 bottles of beer on the wall, 3 bottles of beer.\nTake one down and pass it around, 2 bottles of beer on the wall.\n\n2 bottles of beer on the wall, 2 bottles of beer.\nTake one down and pass it around, 1 bottle of beer on the wall.\n\n1 bottle of beer on the wall, 1 bottle of beer.\nTake it down and pass it around, no more bottles of beer on the wall.\n\nNo more bottles of beer on the wall, no more bottles of beer.\nGo to the store and buy some more, 99 bottles of beer on the wall.\n'.trim();
    expect(song.sing(3)).toEqual(expected);
  });
});
\$\endgroup\$
2
\$\begingroup\$

I've added some more critiques to make this a thorough review. Hopefully you can learn some new techniques in JavaScript by taking these into consideration:

  • You have a bug: You call this.validateArguments(verses) but the signature is validateArguments(...args) {...}. I suggest you change the call to this.validateArguments(...verses) and the signature to validateArguments() {...}, then replace any reference to args with arguments.
  • Consider renaming verses, since using it as the argument in sing() and as the function in BeerSong might cause confusion.
  • It's unnecessary to place a break immediately after a return, even in a switch statement, so remove those.
  • It's generally bad practice to modify the prototype of builtins like String. I've added an interesting alternative below, the Quantity getter.
  • In your unit tests, you should add the expected thrown errors. It is necessary to test them.
  • BottleFactory really should be a static function of Bottle.
  • Bottle should have instance variables for all the methods rather than just static values in overridden functions. This reduces duplication.
  • To increase readability, you can replace the successor() method with the alternative getter implementation below.
  • BeerSong doesn't really make sense as a class, but more as a namespace.
  • Changing verses() to a generator function actually makes things nicer to deal with. Also, set a default to starting of 99 since that is the objective.
  • Don't use return `` + ... when you really just need to use a group.
  • Throw error objects instead of strings. In this case, it would make sense to use RangeError for both exceptions.
  • Semantically, store starting to another variable like current that's iterated instead. Once you decrement starting, it is no longer an appropriate variable name.

All together, these changes would look like this:

beer-song.js

const BeerSong = module.exports = {
  sing(...verses) {
    this.validateArguments(...verses);
    return [...this.recite(...verses)].join('\n\n');
  },

  *recite(starting = 99, ending = 0) {
    let current = starting;

    while(current >= ending) {
      yield this.verse(current--);
    }
  },

  verse(number) {
    const bottle = Bottle.getBottle(number);
    const nextBottle = bottle.successor;

    return (
      `${bottle.Quantity} ${bottle.container} of beer on the wall, ` +
      `${bottle.quantity} ${bottle.container} of beer.\n` +
      `${bottle.action}, ` +
      `${nextBottle.quantity} ${nextBottle.container} of beer on the wall.`
    );
  },

  validateArguments() {
    if (arguments.length > 2) {
      throw new RangeError('Invalid number of arguments: max 2.');
    }
    if (arguments.length == 2 && arguments[0] < arguments[1]) {
      throw new RangeError('Invalid arguments: must be in decreasing order.');
    }
  },
};

class Bottle {
  static getBottle(number) {
    switch (number) {
      case 1:
        return new OneBottle();
      case 0:
        return new ZeroBottle();
      default:
        return new Bottle(number);
    }
  }

  constructor(number, {
    quantity = number.toString(),
    container = 'bottles',
    pronoun = 'one',
    action = `Take ${pronoun} down and pass it around`
  } = {}) {
    this.number = number;
    this.quantity = quantity;
    this.container = container;
    this.pronoun = pronoun;
    this.action = action;
  }

  get Quantity() {
    return this.quantity.charAt(0).toUpperCase() + this.quantity.substr(1);
  }

  get successor() {
    return Bottle.getBottle(this.number - 1);
  }
}

class OneBottle extends Bottle {
  constructor() {
    super(1, {
      container: 'bottle',
      pronoun: 'it'
    });
  }
}

class ZeroBottle extends Bottle {
  constructor() {
    super(0, {
      quantity: 'no more',
      action: 'Go to the store and buy some more'
    });
  }

  get successor() {
    return new Bottle(99);
  }
}

beer-song.spec.js

var song = require('./beer-song');

describe('BeerSong', function() {
  it('prints an arbitrary verse', function() {
    var expected = '8 bottles of beer on the wall, 8 bottles of beer.\nTake one down and pass it around, 7 bottles of beer on the wall.\n';
    expect(song.verse(8)).toEqual(expected);
  });

  it('handles 2 bottles', function() {
    var expected = '2 bottles of beer on the wall, 2 bottles of beer.\nTake one down and pass it around, 1 bottle of beer on the wall.\n';
    expect(song.verse(2)).toEqual(expected);
  });

  it('handles 1 bottle', function() {
    var expected = '1 bottle of beer on the wall, 1 bottle of beer.\nTake it down and pass it around, no more bottles of beer on the wall.\n';
    expect(song.verse(1)).toEqual(expected);
  });

  it('handles 0 bottles', function() {
    var expected = 'No more bottles of beer on the wall, no more bottles of beer.\nGo to the store and buy some more, 99 bottles of beer on the wall.\n';
    expect(song.verse(0)).toEqual(expected);
  });

  it('sings several verses', function() {
    var expected = '8 bottles of beer on the wall, 8 bottles of beer.\nTake one down and pass it around, 7 bottles of beer on the wall.\n\n7 bottles of beer on the wall, 7 bottles of beer.\nTake one down and pass it around, 6 bottles of beer on the wall.\n\n6 bottles of beer on the wall, 6 bottles of beer.\nTake one down and pass it around, 5 bottles of beer on the wall.\n'.trim();
    expect(song.sing(8, 6)).toEqual(expected);
  });

  it('sings the rest of the verses', function() {
    var expected = '3 bottles of beer on the wall, 3 bottles of beer.\nTake one down and pass it around, 2 bottles of beer on the wall.\n\n2 bottles of beer on the wall, 2 bottles of beer.\nTake one down and pass it around, 1 bottle of beer on the wall.\n\n1 bottle of beer on the wall, 1 bottle of beer.\nTake it down and pass it around, no more bottles of beer on the wall.\n\nNo more bottles of beer on the wall, no more bottles of beer.\nGo to the store and buy some more, 99 bottles of beer on the wall.';
    expect(song.sing(3)).toEqual(expected);
  });

  // don't forget to add unit tests for the errors
  // (I'm not even sure what unit testing library this is, so I'll leave that to you)
});
\$\endgroup\$
4
\$\begingroup\$

Your approach:

  • Define duplicate objects that contain information about plural and singular forms and what to do next.

  • Access these from a single conditional (your switch / case) in an object-factory object.

Pros:

  • You have used polymorphism to replace conditionals.

Cons:

  • The scope and hierarchy of bottle objects, and how they apply to the song output, are a little confusing.

A possible alternative:

Your BottleFactory implementation is readable, and I must say innovative. However, the approach creates a lot of extra code that isn't exactly aiding readability or re-use.

You have a BottleFactory, with three bottle-like objects: Bottle, OneBottle and ZeroBottle.

It would be neater not to think of the bottles themselves as objects, but simply values of a counter property applied to a container object, the "wall" in this case. The "wall" object would replace your three bottle-like-objects and bottle-factory, and a single instance would serve for the whole song. Indeed, each verse of the song itself can be thought of as a message accessble to the user about the current status of the "wall", along with the next action to be undertaken.


Super-class constructor

You are creating a new instance of BottleFactory every time the verse method is called. You should move the line const factory = new BottleFactory(); to a constructor function for the BeerSong class, which will run only once.


Design Patterns

Remember the "Beer Song" exercise is supposed to prepare you for real examples. You will need to think more about which things should be objects and which should be their properties and methods.

The goal is usually to get your implementation as close as possible to a standard design pattern. In this case, your program might be one of a general type that displays count-downs and count-ups. Examples of this you can look at in Javascript might be counters (eg. word count or number of lines) in online forms, that are often handled on the client side and can be viewed and downloaded as examples.

Inspiration for a design pattern to use could also come from the song itself. Here, you get a gold star for recognizing the actions implied in the song, and using them as action() methods in your program. The song also inpires a strategy for finishing software projects after work hours (it's not a productive one).


Type-matching

Remove number from:

        return new OneBottle(number);
        break;
      case 0:
        return new ZeroBottle(number);

as you have defined constructors for OneBottle and ZeroBottle that take no arguments.

While this works in Javascript, automated code review / debug tools will check whether arguments applied to a function match the expected type, and give you nasty messages about it. Which they do for very important reasons. Losing track of similar-looking function calls, and what kind of arguments they are supposed to take, will become a cause of problems down the line, especially in languages like Javascript, where mismatches such as extra arguments are handled silently.


BeerWall object

Here is my example of a wall object that handles bottles as subordinate objects.

The BeerWall object contains an array of bottle objects. It would be interfaced with via a song object.

Pros:

  • The actions stated in the output (the song) match exactly what is happening to the BeerWall object. (eg. when a bottle is "taken down" it is popped from the array.)
  • Special cases are defined as properties on bottle objects in the Bottles array, rather than making new sub-classes for them.

Cons: - I have the dumb song stuck in my head.

var plural = ["s", ""]
var short_status = "%n% bottle%p% of beer on the wall."
var long_status = "%n% bottle%p% of beer on the wall, %n% bottle%p% of beer."
var default_action_text = "Take one down and pass it around, "
var buy_text = "Go to the store and buy some more, "

class BeerWall {

    constructor(n) {
        this.fill = n
        this.buy_bottles()
    }

    defs() {
        this.default = {
            action_text : default_action_text,
            action : this.take_one_down,
            plural : 0
        }
        this.bottles[0] = {
            action_text : buy_text,
            action : this.buy_bottles,
            alt_id : "no more"
        }
        this.bottles[1] = {
            plural : 1
        }
    }

    buy_bottles() {
        this.bottles = []
        for (var i = 0; i <= this.fill; i++) {
            this.bottles.push({})
        }
        this.defs()
        return this.output(buy_text + short_status, this.fill)
    }

    take_one_down() {
        var n = this.bottles.length - 1
        var action_text = this.bottles[n].action_text || this.default.action_text
        this.bottles.pop()
        n -= 1
        return this.output(action_text + short_status, n)
    }

    status() {
        return this.output(long_status, this.bottles.length - 1)
    }

    action() {
        var n = this.bottles.length - 1
        var action = this.bottles[n].action || this.default.action
        return action.call(this)  
    }

    output(text,n) {
        return {
            text: text,
            n: this.bottles[n].alt_id || String(n),
            p: plural[this.bottles[n].plural || this.default.plural]
        }
    }
}

// Try it out!!
c = 10    // number of bottles to start with  
var mywall = new BeerWall(c)
for (var i = 0; i <= c; i++) {
    console.log(mywall.status())
    console.log(mywall.action())
}
// The above loop logs all lines for the song as objects of the form:
// {   n: (number of bottles)
//     p: ("s" or "" for plural)
//     text: (appropriate text with placeholders for n and p)   }
// The wall restocks itself and can continue forever.
\$\endgroup\$
  • \$\begingroup\$ Any chance you could elaborate on what you envision related to moving the Bottle objects into a Wall object? I'm a bit unsure of what implementing that might look like why it could be an improvement. I appreciate the feedback! \$\endgroup\$ – jsuth Feb 2 '17 at 6:48
  • \$\begingroup\$ Good point on the type-matching. I noticed that myself but forgot to explicitly state that in my review. I'm also curious about this mutable Wall class, if you could elaborate on that. \$\endgroup\$ – Patrick Roberts Feb 2 '17 at 7:06
  • 1
    \$\begingroup\$ @jsuth this is how I would envision a BeerWall \$\endgroup\$ – Patrick Roberts Feb 2 '17 at 8:53
  • 1
    \$\begingroup\$ @Patrick Roberts - Nice BeerWall! \$\endgroup\$ – Ryan Mills Feb 2 '17 at 13:20

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.