Step 9 - Refactoring into a Service

In the last step I created my HTTP requests inline. In reality for the simple example we used that's fine because there's no serious logic there. However, in order to make it easier to test and abstract the 'service' logic it's a good idea to create a separate service that handles the HTTP requests and errors and transforms the messages into some thing more usable than an Http response.

Creating a Service

Let's create a new service called ToDoService.ts in the same folder as the todo component.

import { Injectable } from '@angular/core';
import { Http } from '@angular/http';
import {Observable} from 'rxjs';
import {TodoItem} from "./todo";

@Injectable()
export class TodoService {

	baseUrl = "http://localhost/todo/";

    constructor(private http:Http) {
    }

    getTodos() {
	    return this.http.get(this.baseUrl + "todos.td")
		    .map( (response) => {
		    	// transform the response
			    // into an object
			    return response.json();
		    })
		    .catch( (response) => {
		    	// rethrow the error as something easier to work
			    // with. Normally you'd return an error object
			    // with parsed error info
			    return Observable.throw("failed to get todo items");
		    });
    }

    addTodo(todo:TodoItem) {
    	return this.http.put(this.baseUrl + "todo.td",todo)
		    .map( response => {
		    	return true;
		    })
		    .catch( response => {
		    	return Observable.throw("Failed to save to do item: " + todo.title);
		    });
    }

    removeTodo(todo:TodoItem) {
	    return this.http.delete(this.baseUrl + "todo.td?title=" + todo.title)
		    .map(response => {
			    return true;
		    })
		    .catch(response => {
			    return Observable.throw( "Failed to save to do item: " + todo.title);
		    });

    }
}

A service is just a Typescript class and behaves like any other. The Injectable() meta tag sets up the class so it can be dependency injected into other components. A service is supposed to be a wrapper around business logic - in most cases this means it wraps a service and manages pre-processing of the data that comes back from the service, and handling errors in such a way they are easy to consume in the front end. In this sample, we don't have any complex logic but even in this simple example you can see what the service methods accomplish:

  • Pick up the response data
  • Repackage to data in a format that is easy to consume by the application
  • Catches errors and returns the errors in some easy to consume format

If you look at getTodos() it returns the array of TodoItemp[] rather than a response object. The front end doesn't need to know how the data arrived, it just needs the array to work with. By doing so you hide the mechanics of how the data is provided and if in the future the underlying features change you can change it in one place.

The service returns Observable objects to the caller, so the caller will end up calling .subscribe() on this service. Your code can use Observable functions like .map() or .flatMap() which intercept the requests before they get sent back to the subscriber. You can do all sorts of processing on the event or events that pass through. You can use .catch() to capture any errors that occur in the pipeline and effectively package up the result using return Observable.throw( newResult ); to transform the error response - the client will get the error with this object as a parameter.

Add the service to the Module list

Once the service exists it has to be declared. As with all injectable components make sure to register it for the module loader in app.module.ts:

import {TodoService} from "./todo/todoService";

and

providers   : [
	TodoService,
	...
  ],

Provider/Service lifetime - Singleton or Transient

Each service that is to be injected has to be declared as a provider at some level of the component tree. By defining the service at the top level module level the service is essentially a Singleton - it gets instantiated once and then stays loaded. When a constructor requests the service it gets the same instance. This means that if you can create properties on the service with data and that data will persist - even across many components. This allows for data sharing and data caching when views are reactivated.

You can declare providers on any component and so you can declare a service locally on a component which means a new instance will be loaded each time.

In short - you can choose what lifetime a service has - all the way from Singleton (off the root component) or completely transient if you declare it on the active component you're working on.

Calling the Service

Ok now that we have the service we can call it from the Todo component. The code gets just a little simpler and little more focused which is as it should be for front end code. Here are the three methods that use the service:

loadTodos() {
	this.service.getTodos()
		.subscribe( todos => {
			this.todos = todos;
		}, msg => {
			this.showError(msg);
		});
}

removeTodo(todo:TodoItem) {
    this.service.removeTodo(todo)
	    .subscribe( result => {
			    this.todos = this.todos.filter((t, i) => t.title != todo.title)
		    }, msg => {
			    this.showError(msg);
		    });
}

addTodo(todo,form) {

    this.service.addTodo(todo)
	    .subscribe((result)=> {
			    this.todos.splice(0, 0, todo);
			    this.activeTodo = new TodoItem();
			    form.reset();
		    }, msg => {
			    this.showError(msg);
		    });
}

While this code doesn't look much different than what we had before because the logic is very simple, it is simpler. No longer does this code have to worry about URLs or parsing a response object. It just gets the actual data or an error message back as a result.


© West Wind Technologies, 1996-2022 • Updated: 09/19/16
Comment or report problem with topic