Angular Project Blackjack: 5 – Player Service

(This post is part of my “from scratch” AngularJS project. If you are feeling lost, the first post is here.)

Now that we have used views, directives and controllers, let’s move on to another big part of the AngularJS library, “Services”. If you’ve used angular for any amount of time, I’m pretty sure you’ve already googled “What is the difference between service, factory and provider?”. I know I have and I still haven’t fully grasped it yet. The best answer I can give is that if you need it to be runtime configurable, use a provider. Otherwise, use a factory. Why? Because John Papa said to! Services and factories are nearly identical, but using the revealing module pattern, you can make them really look like header and implementation files, which makes the Objective-C developer in me very happy!

(One thing to note, I’ll use the word “Service” throughout this post and even name my factory “PlayerService”. This is because it makes more sense in the usage of these factories elsewhere in the code to call it a service. When you read “Inject the NetworkService”, you understand what that code object does better than calling it “Inject the NetworkFactory”.)

In our blackjack game, we will have a player who is playing the game. This player will have points/money and a name. Assigning the player a name and adding/subtracting points will all need logic somewhere. We could simply define all of this code in the Game Controller, but, that goes against our single responsibility rule. Instead, let’s make this into a PlayerService!

Our player service is going to live in a new folder, /src/app/player. We’ll create two files, the module for the folder (player.module.jsand the service (player.service.js). We’re going to go a little bit deeper into the code for the service because it requires us to make a javascript object class as well.

(function(){
    'use strict';

    angular
        .module('blackjack.player')
        .factory('PlayerService', PlayerService);

    function PlayerService(){
        var service = {
            newPlayer: newPlayer
        };

        /////////////

        function Player(playerName, initialScore) {
            this.score = initialScore;
            this.name = playerName;
        }

        Player.prototype.changeScore = function(amountToChange){
            if(!angular.isDefined(this.score)){
                this.score = 0;
            }

            this.score += amountToChange;
        };

        Player.prototype.resetScore = function () {
            this.score = 0;
        };

        function newPlayer(playerName, initialScore) {
            var player = new Player(playerName, initialScore);
            return player;
        }

        return service;
    }
})();

Right now, the only function available to PlayerService is ‘newPlayer’. This will create a new javascript ‘Player’ object and return it. We also have some (what we would call in OOP, ‘class methods’) prototype functions on the objects. These are the functions that we will call from our controller to modify our player object. Our ‘newPlayer’ function on the PlayerService creates the Player object and sets the parameters that are passed in to it.

Now that we have our service laid out, how do we go about using it? Well, once we update our index.html and our main blackjack.module.js to include our new service, we can simply inject our player service into the game controller. Our game.controller.js:

(function(){
    'use strict';

    angular
        .module('blackjack.game')
        .controller('GameController',GameController);

    GameController.$inject = ['PlayerService'];

    function GameController(PlayerService){
        var game = this;

        game.init = function () {
            game.started = false;
            game.player = PlayerService.newPlayer('Ringo', 100);
        };

        game.start = function () {
            game.started = true;
            game.player.changeScore(-100);
        };

        game.end = function () {
            game.started = false;
            game.player.changeScore(200);
        };

        game.reset = function () {
            game.player.resetScore();
            game.started = false;
        };

        game.init();
    }
})();

The two big lines to pay attention to here are:

GameController.$inject = ['PlayerService'];
function GameController(PlayerService){

This is AngularJS’s injection. The PlayerService is now available throughout our GameController and can be called from within it. Angular provides us with a service locator that will find these services simply by updating $inject.

We now have our player get created with a new game initialization and he’s winning his blackjack hand every time we “end” the game. Everything seems to be working great. That is, until we go back and run our unit tests and see we have a “Error: [$injector:unpr]” error being reported. That is because we haven’t given our game controller tests access to the PlayerService. Give it access to it by adding it to the listed modules it loads:

module('blackjack.player');

While we’re in there, let’s add a quick test as well:

it('should have a player defined', function (){
    expect(gameController.player).toBeDefined();
});

Since we’re writing tests, might as well write some for our PlayerService as well:

'use strict';

describe('PlayerService Unit Tests', function () {
    var PlayerService;
    beforeEach(function () {
        module('blackjack.player');
        inject(function (_PlayerService_) {
            PlayerService = _PlayerService_;
        });
    });

    it('should be true', function () {
        expect(true).toBe(true);
    });

    it('should create a new player with name and score', function () {
        var playerName = 'testPlayer';
        var playerScore = 10101;
        var player = PlayerService.newPlayer(playerName, playerScore);
        expect(player.name).toBe(playerName);
        expect(player.score).toBe(playerScore);
    });

});

One interesting thing to notice in our testing code is the weird “_PlayerService_” way of doing the injection. By surrounding the service with underscores, Angular knows to call the actual service here. When we assign the underscored version of PlayerService to our local var PlayerService, we are giving our test a copy of that service from within the test. This is important because it allows us to do things like mock results, change functions, etc., all from within our test. The reason it makes a copy is so that if the service is injected in other tests, our modifications aren’t passed over to them and they get a “clean” version of the service.

Our tests should be passing now with no errors. You can visit the code base here.

Up Next: TDD Card Service

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s