Services

It is time to get into the more cerebral part of our budding little app. So far, we have a system set up, that handles the routing, but just returns mocks. Today, we will still return mocks, but from a level deeper. So, without further ado, I give you…

A mock repository.

// /repositories/index.js

const todo = require('./todo');

module.exports = globals => ({
  todo: todo(globals),
});

and

// /repositories/todo.js

const Promise = require('bluebird');

const mockTodo = (id, body) => (Object.assign({
  id: `${id || Math.ceil(Math.random() * 1000)}`,
  type: 'todo',
  content: 'my random Todo',
}, body));

module.exports = () => ({
  get: ({ id }) => query => body => (!id
    ? Promise.resolve([mockTodo(id), mockTodo(id + 1)])
    : Promise.resolve([mockTodo(id)])),

  save: ({ id }) => query => body  => Promise.resolve([1]),

  delete: params => query => body => Promise.resolve([1]),
});

“But, Ben, I thought we were going to do services today? I really had my heart and mind set on that… Also, what’s up with all those chained arrows?”

Yes, yes, we are going to do services today. But, first, we had to give services something to work with. Since services are “just” the glue that links various parts of the application, it is easier to work on them if we have at least something prepared that they can work with.

And as for the chained arrows,

params => query => body => Promise.resolve([1])

is equivalent (ignoring the differences between => and function, which you should google, as they are important) to

function (params) {
  return function (query) {
    return function (body) {
      return Promise.resolve([1])
    }
  }
}

If you are wondering, what demon possessed me to do something like this, well, it is the way I got used to doing things. It may or may not be the best way, but it is, in my experience, a very functional way because, you see, the arguments to all three functions are not really at the same hierarchical level.

I will first need to know which model I am working with, and I get that piece of information from params. Perhaps I have to do some special processing of the data I get in params. If that is async, I can simply call the whole chain with params and pass the function that I get returned as a callback to my theoretical async params processor.

Once I know “who” I am working with, I check if there are any “metadata” there by checking the query parameters. I can then again do some processing of query and pass the final returned function that takes body as a callback.

The good part of all this is that, thanks to the gloriousness of closures, all three arguments will be available within the innermost function.

As said, however, you can also implement everything I just described with a single function that simply does things in promises.

But, now it really is time for services.

// /services/index.js

const todo = require('./todo');

module.exports = globals => ({
  todo: todo(globals),
});

Does this look familiar? It should, it is just a way to get our services, like repositories before, nicely exported.

// /services/base.js

module.exports = globals => serviceName => ({
  service: serviceName,

  get: params => query => body =>
    globals.repositories[serviceName].get(params)(query)(body),

  create: params => query => body =>
    globals.repositories[serviceName].save(params)(query)(body),

  update: params => query => body =>
    globals.repositories[serviceName].save(params)(query)(body),

  delete: params => query => body =>
    globals.repositories[serviceName].delete(params)(query)(body),
});

I take it what this file generally does is quite clear?

Base? What base?

Alright. For now, we only have users and todos but, at some point, there may be many more endpoints, which have the same general logic, “just get something and do not worry about it”, so writing an identical get method for every service would be both a waste of our time and the anathema of keeping things DRY. Therefore, we will create a set of base methods we can (re-)use to our heart’s content and extend them if need be, as you will soon see. Basically, you can think of this as the “prototype” for our services.

So, here is the last new file:

// /services/todo.js

const baseServices = require('./base');

module.exports = (globals) => {
  const base = baseServices(globals)('todo');

  return Object.assign({}, base, {
    create: params => query => body =>
      globals.repositories.todo
      .save(params)(query)(body)
      .then(([id]) => base.get({ id })(query)(body)),
    });
};

See? Thanks to the beauty of concatenative inheritance, we get our default methods straight from base.js, and simply override the create method, so it returns the newly-created object.

This last sentence is the essence of “application logic”. The service is not interested in how the data is saved or retrieved. Nor does it concern itself with how the data is going to be returned to the user, or whence it came.

The service has one job, and that is to

  1. take in certain data,

  2. pass it between all the relevant parts of the app for processing

  3. return it to whichever API requested it — in our case this is a REST API, but it could have just as easily been e.g. a WebSockets interface.

And this is the point of application layers. Each layer only cares about its duties and does not interfere with other layers’ duties. This is what keeps everything maintainable and easily replaceable.

Now to get things actually working, we also have to modify some existing files, most notably /controllers/rest/router/routes/todo.js, where we change our callbacks so they actually call our services’ methods.

The “get all todos” route’s callback changes from

cb: (params, query, body) =>
  Promise.resolve([mockTodo(params.id), mockTodo(params.id + 1)]),

to

cb: (params, query, body) =>
  globals.services.todo.get(params)(query)(body),

Try to figure out the others yourself. And do not forget to remove the

const Promise = require('bluebird');

let mockTodo = (id, body) => (Object.assign({
    id: `${id || Math.ceil(Math.random() * 1000)}`,
    type: 'todo',
    content: 'my random Todo',
  }, body));

part at the top. We do not need that anymore.

Finally, we have to get repositories and services loaded into our globals object, which we do in /index.js.

Just below the const globals initialization, but before the require for the router, add:

globals.repositories = require('./repositories')(globals);
globals.services = require('./services')(globals);

Voilá, we are good to go.

So, to sum up, what we have learned today:

  1. How to do a simple mock repository. This can come in handy for testing purposes, and since our whole architecture is based on passing functions, those mocks can easily be used, exchanged etc.

  2. How to do concatenative inheritance. Remember how we created a “prototype” base service with default methods we then cloned and extended with a custom create method in the todos service?

  3. Why chaining functions that return functions can be a good idea.

  4. What services actually do, even if only a little — we had a service create an entry in our system first and then retrieve it again.

This is it for today. In the next chapter, we will add the final layer — repositories, which will take care of saving everything to the database.

Services — Github Repository

results matching ""

    No results matching ""