Should simply access data, and be stateless (with the exception of caching). With few exceptions, no singleton service should really ever have a “setter”.
Most services are used to pull data from a remote server (which will be the topic of this article), but services may be used for many other things as well (logging, etc.)
Why put data access in a service rather than on a page?: We want to be able to retrieve remote/stored data from a single place that can be called from multiple pages. Also, if our data source changes, then we only need to make that change in one place.
Why not simply put $http calls on my pages?: We will be doing post-processing and translation of raw HTTP response data, and this will not be the responsibility of our page controllers. We want to keep our page controllers as light as possible.
One thing services should generally not do however is modify the DOM. (One notable exception though is to display an application-level alert modal, for instance.)
Service Diagram (from High Level Overview)
2. HTTP Request to Server
|
| +------------+
+-----------------+ v | |
| | | |
| (Service) +-----------> |
| | | Server |
| Heroes Service <- - - - - -+ |
| | | |
+----^-----+------+ ^ | |
| | | +------------+
| |
| | 3. HTTP Response Received
|
1. Data Requested from Service -> | |
|
| | <- 4. Data Models Returned
|
| |
++======+=====v=======++
|| ||
|| (Page) ||
|| ||
|| Heroes Dashboard ||
|| ||
|| ||
|| ||
++====================++
All data retrieval from a data source (a remote server, local storage, etc.) should be encapsulated in a service. This allows multiple pages to access your service.
Services also allow for a data abstraction layer, where you will convert raw HTTP responses into known objects that the app can consume. More on that later.
Example Service, HeroesService
Generally, use the factory recipe to create singleton services.
angular.module( 'heroes' ).factory( 'HeroesService', function( $http, HeroesReader, HeroesWriter ) {
'use strict';
// Public API
return {
loadHeroes : loadHeroes,
loadHero : loadHero,
saveHero : saveHero
};
/**
* Loads the list of heroes.
*
* @return {Promise} A promise which is resolved with an array of
* {@link Hero} objects, or is rejected if an error occurs.
*/
function loadHeroes() {
return $http( { url: '/path/to/heroes' } ).then(
function onSuccess( response ) {
return HeroesReader.readHeroes( response.data );
}
);
}
/**
* Loads a Hero by ID.
*
* @param {Number} heroId The ID of the hero to load.
* @return {Promise} A promise which is resolved with a {@link Hero} object
* when successful, or is rejected if an error occurs.
*/
function loadHero( heroId ) {
return $http( { url: '/path/to/hero/' + heroId } ).then(
function onSuccess( response ) {
return HeroesReader.readHero( response.data );
}
} );
}
/**
* Saves a Hero to the server.
*
* @param {Hero} hero The Hero to save.
* @return {Promise} A promise which is resolved with an updated {@link Hero}
* when the operation is complete, or is rejected if an error occurs.
*/
function saveHero( hero ) {
var data = HeroWriter.write( hero );
return $http( {
url : '/path/to/hero' + ( !hero.id ? '' : '/' + hero.id ),
method : !hero.id ? 'POST' : 'PUT',
data : HeroesWriter.writeHero( hero )
} ).then( function( response ) {
return HeroesReader.readHero( response.data );
} );
}
} );
Why
loadHeroes()instead ofgetHeroes()?: "get" would make the method sound like it's going to return a usable value rather than an async promise, whereas "load" notifies developers to the asynchronous nature.Why the
factoryrecipe?: It allows you to expose the public methods, and keep all methods as simple function declarations so that you can call them from other public methods without worrying about thethisreference.Why
HeroesReader?: See next section.
HeroesReader
We don't want to simply return raw HTTP responses to be consumed by our app. This would tightly couple our pages and components to exactly what the server provides (which may change over time). Instead, we want to abstract away the server and represent our data with model classes.
Why?: Using a class to represent a data model allows that data model to be easily documented across your system within the functions that data model is passed to.
Why?: A model's class definition immediately tells all developers exactly which properties are going to be available on that object. With good documentation (or even better, TypeScript), they also tell other developers which data types the properties hold.
Why?: Data models/collections as classes allows you to add functionality in the form of accessor/query methods. These methods can provide additional information, or implement logic to provide data to the consumer that the properties alone do not make obvious, or do not provide.
HeroesReader has the responsibility of converting raw HTTP responses into
your data models.
Example HeroesReader:
angular.module( 'heroes' ).factory( 'HeroesReader', function( Hero ) {
'use strict';
// Public API
return {
readHeroes : readHeroes,
readHero : readHero
};
/**
* Reads the server response data for heroes.
*
* Example server response data:
*
* [
* { id: 11, name: 'Mr. Nice', lastBattle: '2016-04-22T14:22:01' },
* { id: 12, name: 'Narco', lastBattle: '2016-02-01T08:52:27' },
* { id: 13, name: 'Bombasto', lastBattle: '2016-05-10T18:00:52' }
* ]
*
* @param {Object[]} heroObjs The array of 'hero' objects from the server.
* @return {Heroes[]}
*/
function read( heroObjs ) {
return _.map( heroObjs, readHero );
}
/**
* Reads the server response data for a single Hero.
*
* Example server response data:
*
* { id: 11, name: 'Mr. Nice', lastBattle: '2016-04-22T14:22:01' }
*
* @param {Object} heroObj The 'hero' object from the server.
* @return {Hero} A Hero instance.
*/
function readHero( heroObj ) {
return new Hero( {
id : heroObj.id,
name : heroObj.name,
lastBattle : moment( heroObj.lastBattle )
} );
}
} );
Why do this?: A Reader gives you the perfect place to parse server responses into consumable objects. For example, the
lastBattleabove is converted into a Moment.js object for the app to consume and format as each view desires.Why not put this into the Service itself?: Object instantiation is a responsibility all on its own (See the Single Responsibility Principle of software design), and therefore we should have only one place to change if the server-side data changes.
Why not put this into the Service itself (2)?: If your service starts to garner more data retrieval methods, and also needs parsing logic/methods to convert those responses into reasonable client-side models, then your service starts to become difficult to read and understand.
Why not put this into the Service itself (3)?: When you start writing tests for your components, and want to use test/example data that is in the form of server responses to populate your components, you can use your Reader class to generate the models from the data to pass into your view components.
Hero Model
Here's an example of a model "class" in ES5, but if you're able, use ES6 classes and Babel to transpile them into ES5. Or, even better, use TypeScript classes.
angular.module( 'heroes' ).factory( 'Hero', function() {
'use strict';
/**
* @class Hero
*
* Represents a Hero in the app.
*
* @constructor
* @param {Object} props An Object containing the following properties:
* @param {Number} props.id The hero's ID.
* @param {String} props.name The hero's name.
* @param {Moment} props.lastBattle The date/time of the hero's last
* battle.
*/
var Hero = function( props ) {
this.id = props.id;
this.name = props.name;
this.lastBattle = props.lastBattle;
};
/**
* Determines if the Hero has battled recently.
*
* @return {Boolean}
*/
Hero.prototype.hasBattledRecently = function() {
var threeMonthsAgo = moment().subtract( 3, 'months' );
return this.lastBattle.isAfter( threeMonthsAgo );
}
return Hero;
} );
See the next article (Models and Collections) for more details.
For how to test, see Testing Services.
Next Article: Models and Collections