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);
});
});